local-diff-reviewer 4.0.0 → 4.0.2
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/README.md +7 -1
- package/SKILL.md +1 -1
- package/dist/cli/start.js +268 -126
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -143,7 +143,13 @@ npx --yes local-diff-reviewer \
|
|
|
143
143
|
|
|
144
144
|
`thread` 评论会以 `author: "agent"` 写入:如果同一锚点已经存在 thread,会作为新的 comment 追加进去;否则创建 replied thread。`reply` 会向目标 thread 追加一条 agent 回复并把状态切到 replied。为避免 agent finding 反复注入导致刷屏,同一 thread 内相同正文的 agent comment 会被视为重复并跳过。若路径不在当前 diff 中、行号无法定位或内容重复,脚本会跳过并在终端打印 warning。
|
|
145
145
|
|
|
146
|
-
当 agent 收到从 UI 复制出的 `[thread:<id>]` prompt 并完成处理后,应使用 `type: "reply"` 把处理结果写回原 thread,作为 `author: "agent"` 的 comment
|
|
146
|
+
当 agent 收到从 UI 复制出的 `[thread:<id>]` prompt 并完成处理后,应使用 `type: "reply"` 把处理结果写回原 thread,作为 `author: "agent"` 的 comment 保留在评论流里。回复内容默认保持结论式、尽量短:
|
|
147
|
+
|
|
148
|
+
- 已按评论完成修改时,优先使用 `已处理`。
|
|
149
|
+
- 需要补充一点点定位信息时,可使用 `已处理:xxx`。
|
|
150
|
+
- 未修改时,再简短说明原因,例如 `未处理:当前实现已覆盖该场景。`
|
|
151
|
+
|
|
152
|
+
除非用户明确需要更详细的回写说明,否则不要重复整段 diff、实现细节或长篇解释。
|
|
147
153
|
|
|
148
154
|
从 UI 复制出的 prompt 示例:
|
|
149
155
|
|
package/SKILL.md
CHANGED
|
@@ -36,7 +36,7 @@ npx --yes local-diff-reviewer [args...] \
|
|
|
36
36
|
|
|
37
37
|
After a newly started script prints a local URL, open it in the Codex browser when available. If the script reports `Diff Review refreshed`, do not open another page: the already open review page updates automatically. If browser automation is not available, report the URL.
|
|
38
38
|
|
|
39
|
-
When the user gives you copied prompt text containing `[thread:<id>]`, treat it as an existing review thread. After you answer or make code/doc changes for that thread, append a concise agent reply to the same thread with `--comment '{"type":"reply","threadId":"<id>","body":"..."}'` the next time you launch or refresh the viewer. If the viewer is already running and you can reach the API, post the same reply to `/api/threads/<id>/comments` with `author: "agent"`. Do this for every handled thread unless the user explicitly asks you not to write back.
|
|
39
|
+
When the user gives you copied prompt text containing `[thread:<id>]`, treat it as an existing review thread. After you answer or make code/doc changes for that thread, append a concise agent reply to the same thread with `--comment '{"type":"reply","threadId":"<id>","body":"..."}'` the next time you launch or refresh the viewer. If the viewer is already running and you can reach the API, post the same reply to `/api/threads/<id>/comments` with `author: "agent"`. Do this for every handled thread unless the user explicitly asks you not to write back. Prefer conclusion-first, minimal replies such as `已处理`, `已处理:xxx`, or `未处理:原因...`; do not repeat the full diff or long implementation details unless the user explicitly wants that detail.
|
|
40
40
|
|
|
41
41
|
## Review Scope
|
|
42
42
|
|
package/dist/cli/start.js
CHANGED
|
@@ -8,7 +8,7 @@ import { basename as basename3, dirname as dirname3, join as join5, resolve as r
|
|
|
8
8
|
import { fileURLToPath } from "node:url";
|
|
9
9
|
|
|
10
10
|
// src/server/storage.ts
|
|
11
|
-
import { mkdir, readFile, writeFile } from "node:fs/promises";
|
|
11
|
+
import { mkdir, readFile, rename, rm, writeFile } from "node:fs/promises";
|
|
12
12
|
import { createHash } from "node:crypto";
|
|
13
13
|
import { homedir, platform } from "node:os";
|
|
14
14
|
import { basename, dirname, join } from "node:path";
|
|
@@ -46,38 +46,71 @@ function getMergedThreadStatus(threads) {
|
|
|
46
46
|
}
|
|
47
47
|
|
|
48
48
|
// src/server/storage.ts
|
|
49
|
+
var commentWriteQueues = /* @__PURE__ */ new Map();
|
|
49
50
|
async function readComments(repoRoot) {
|
|
50
51
|
return readCommentStore(repoRoot);
|
|
51
52
|
}
|
|
52
|
-
async function
|
|
53
|
-
const
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
53
|
+
async function updateComments(repoRoot, updater) {
|
|
54
|
+
const previousQueue = commentWriteQueues.get(repoRoot) ?? Promise.resolve();
|
|
55
|
+
const queuedWrite = previousQueue.catch(() => void 0).then(async () => {
|
|
56
|
+
const store = await readCommentStore(repoRoot);
|
|
57
|
+
const { changed, result } = await updater(store);
|
|
58
|
+
if (changed) {
|
|
59
|
+
await writeComments(repoRoot, store);
|
|
59
60
|
}
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
61
|
+
return result;
|
|
62
|
+
});
|
|
63
|
+
const queueTail = queuedWrite.then(() => void 0, () => void 0);
|
|
64
|
+
commentWriteQueues.set(repoRoot, queueTail);
|
|
65
|
+
try {
|
|
66
|
+
return await queuedWrite;
|
|
67
|
+
} finally {
|
|
68
|
+
if (commentWriteQueues.get(repoRoot) === queueTail) {
|
|
69
|
+
commentWriteQueues.delete(repoRoot);
|
|
64
70
|
}
|
|
65
71
|
}
|
|
66
|
-
|
|
72
|
+
}
|
|
73
|
+
async function attachLegacyComments(repoRoot, diffHash2, diffFiles) {
|
|
74
|
+
await updateComments(repoRoot, (store) => {
|
|
75
|
+
let changed = false;
|
|
76
|
+
for (const thread of store.threads) {
|
|
77
|
+
if (!thread.diffHash) {
|
|
78
|
+
thread.diffHash = diffHash2;
|
|
79
|
+
changed = true;
|
|
80
|
+
}
|
|
81
|
+
const file = diffFiles.find((item) => item.path === thread.filePath);
|
|
82
|
+
if (!thread.fileSnapshotHash && file && (thread.diffHash === diffHash2 || getThreadStatus(thread) === "submit")) {
|
|
83
|
+
thread.fileSnapshotHash = file.snapshotHash;
|
|
84
|
+
changed = true;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
return { changed, result: void 0 };
|
|
88
|
+
});
|
|
67
89
|
}
|
|
68
90
|
async function readCommentStore(repoRoot) {
|
|
69
91
|
const path = commentsPath(repoRoot);
|
|
70
92
|
try {
|
|
71
|
-
|
|
72
|
-
|
|
93
|
+
const text = await readFile(path, "utf8");
|
|
94
|
+
return normalizeStore(parseCommentStore(text));
|
|
95
|
+
} catch (error) {
|
|
96
|
+
if (!isMissingFileError(error)) {
|
|
97
|
+
const recovered = await recoverCommentStore(repoRoot, path, error);
|
|
98
|
+
if (recovered) return recovered;
|
|
99
|
+
}
|
|
73
100
|
return readLegacyComments(repoRoot);
|
|
74
101
|
}
|
|
75
102
|
}
|
|
76
103
|
async function writeComments(repoRoot, store) {
|
|
77
104
|
const path = commentsPath(repoRoot);
|
|
105
|
+
const tempPath = `${path}.${process.pid}.${crypto.randomUUID()}.tmp`;
|
|
78
106
|
await mkdir(dirname(path), { recursive: true });
|
|
79
|
-
|
|
107
|
+
try {
|
|
108
|
+
await writeFile(tempPath, `${JSON.stringify(store, null, 2)}
|
|
80
109
|
`, "utf8");
|
|
110
|
+
await rename(tempPath, path);
|
|
111
|
+
} finally {
|
|
112
|
+
await rm(tempPath, { force: true }).catch(() => void 0);
|
|
113
|
+
}
|
|
81
114
|
}
|
|
82
115
|
function commentsPath(repoRoot) {
|
|
83
116
|
const repoName = basename(repoRoot) || "repo";
|
|
@@ -92,11 +125,71 @@ function commentLogsDir() {
|
|
|
92
125
|
}
|
|
93
126
|
async function readLegacyComments(repoRoot) {
|
|
94
127
|
try {
|
|
95
|
-
return normalizeStore(
|
|
128
|
+
return normalizeStore(parseCommentStore(await readFile(join(repoRoot, ".diff-review", "comments.json"), "utf8")));
|
|
96
129
|
} catch {
|
|
97
130
|
return { threads: [] };
|
|
98
131
|
}
|
|
99
132
|
}
|
|
133
|
+
async function recoverCommentStore(repoRoot, path, cause) {
|
|
134
|
+
try {
|
|
135
|
+
const recovered = normalizeStore(parseRecoverableCommentStore(await readFile(path, "utf8")));
|
|
136
|
+
await writeComments(repoRoot, recovered);
|
|
137
|
+
return recovered;
|
|
138
|
+
} catch {
|
|
139
|
+
console.warn(`Failed to read comment store ${path}: ${cause instanceof Error ? cause.message : String(cause)}`);
|
|
140
|
+
return void 0;
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
function parseCommentStore(text) {
|
|
144
|
+
const parsed = JSON.parse(text);
|
|
145
|
+
if (!Array.isArray(parsed.threads)) return { threads: [] };
|
|
146
|
+
return parsed;
|
|
147
|
+
}
|
|
148
|
+
function parseRecoverableCommentStore(text) {
|
|
149
|
+
const end = findRootJsonObjectEnd(text);
|
|
150
|
+
if (end === -1) throw new Error("Comment store does not contain a complete JSON object");
|
|
151
|
+
return parseCommentStore(text.slice(0, end));
|
|
152
|
+
}
|
|
153
|
+
function findRootJsonObjectEnd(text) {
|
|
154
|
+
let depth = 0;
|
|
155
|
+
let inString = false;
|
|
156
|
+
let escaped = false;
|
|
157
|
+
let started = false;
|
|
158
|
+
for (let index = 0; index < text.length; index += 1) {
|
|
159
|
+
const char = text[index];
|
|
160
|
+
if (!started) {
|
|
161
|
+
if (/\s/.test(char)) continue;
|
|
162
|
+
if (char !== "{") return -1;
|
|
163
|
+
started = true;
|
|
164
|
+
}
|
|
165
|
+
if (inString) {
|
|
166
|
+
if (escaped) {
|
|
167
|
+
escaped = false;
|
|
168
|
+
} else if (char === "\\") {
|
|
169
|
+
escaped = true;
|
|
170
|
+
} else if (char === '"') {
|
|
171
|
+
inString = false;
|
|
172
|
+
}
|
|
173
|
+
continue;
|
|
174
|
+
}
|
|
175
|
+
if (char === '"') {
|
|
176
|
+
inString = true;
|
|
177
|
+
continue;
|
|
178
|
+
}
|
|
179
|
+
if (char === "{") {
|
|
180
|
+
depth += 1;
|
|
181
|
+
continue;
|
|
182
|
+
}
|
|
183
|
+
if (char === "}") {
|
|
184
|
+
depth -= 1;
|
|
185
|
+
if (depth === 0) return index + 1;
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
return -1;
|
|
189
|
+
}
|
|
190
|
+
function isMissingFileError(error) {
|
|
191
|
+
return typeof error === "object" && error !== null && "code" in error && error.code === "ENOENT";
|
|
192
|
+
}
|
|
100
193
|
function normalizeStore(store) {
|
|
101
194
|
const groups = /* @__PURE__ */ new Map();
|
|
102
195
|
for (const thread of store.threads) {
|
|
@@ -132,36 +225,35 @@ function earliestTimestamp(values) {
|
|
|
132
225
|
async function importAgentComments(repoRoot, diffHash2, diffFiles, rawComments) {
|
|
133
226
|
const result = { imported: 0, skipped: [] };
|
|
134
227
|
if (rawComments.length === 0) return result;
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
228
|
+
await updateComments(repoRoot, (store) => {
|
|
229
|
+
for (let index = 0; index < rawComments.length; index += 1) {
|
|
230
|
+
const label = `--comment #${index + 1}`;
|
|
231
|
+
const parsed = parseImportComment(rawComments[index], label, result);
|
|
232
|
+
if (!parsed) continue;
|
|
233
|
+
if (parsed.type === "reply") {
|
|
234
|
+
if (appendAgentReply(store.threads, parsed, result, label)) {
|
|
235
|
+
result.imported += 1;
|
|
236
|
+
}
|
|
237
|
+
continue;
|
|
143
238
|
}
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
239
|
+
const thread = buildAgentThread(parsed, diffHash2, diffFiles, result, label);
|
|
240
|
+
if (!thread) continue;
|
|
241
|
+
const existingThread = store.threads.find((item) => item.fileSnapshotHash === thread.fileSnapshotHash && sameAnchor(item.anchor, thread.anchor));
|
|
242
|
+
if (hasDuplicateAgentComment(store.threads, thread)) {
|
|
243
|
+
result.skipped.push(`${label}: duplicate agent comment skipped`);
|
|
244
|
+
continue;
|
|
245
|
+
}
|
|
246
|
+
if (existingThread) {
|
|
247
|
+
existingThread.comments.push(thread.comments[0]);
|
|
248
|
+
existingThread.status = getOpenThreadStatus(existingThread);
|
|
249
|
+
existingThread.updatedAt = thread.updatedAt;
|
|
250
|
+
} else {
|
|
251
|
+
store.threads.push(thread);
|
|
252
|
+
}
|
|
253
|
+
result.imported += 1;
|
|
159
254
|
}
|
|
160
|
-
result.imported
|
|
161
|
-
}
|
|
162
|
-
if (result.imported > 0) {
|
|
163
|
-
await writeComments(repoRoot, store);
|
|
164
|
-
}
|
|
255
|
+
return { changed: result.imported > 0, result: void 0 };
|
|
256
|
+
});
|
|
165
257
|
return result;
|
|
166
258
|
}
|
|
167
259
|
function parseImportComment(raw, label, result) {
|
|
@@ -852,6 +944,7 @@ function getThreadLocation(thread) {
|
|
|
852
944
|
|
|
853
945
|
// src/server/file-watcher-service.ts
|
|
854
946
|
import { watch } from "node:fs";
|
|
947
|
+
var IGNORED_REPO_PATH_PREFIXES = ["node_modules", "dist"];
|
|
855
948
|
var FileWatcherService = class {
|
|
856
949
|
constructor(repoRoot) {
|
|
857
950
|
this.repoRoot = repoRoot;
|
|
@@ -927,7 +1020,10 @@ var FileWatcherService = class {
|
|
|
927
1020
|
}
|
|
928
1021
|
tryWatch(path, options) {
|
|
929
1022
|
try {
|
|
930
|
-
const watcher = watch(path, options ?? {}, () => {
|
|
1023
|
+
const watcher = watch(path, options ?? {}, (_eventType, filename) => {
|
|
1024
|
+
if (path === this.repoRoot && this.shouldIgnoreRepoChange(filename)) {
|
|
1025
|
+
return;
|
|
1026
|
+
}
|
|
931
1027
|
this.handleChange();
|
|
932
1028
|
});
|
|
933
1029
|
this.watchers.push(watcher);
|
|
@@ -941,6 +1037,14 @@ var FileWatcherService = class {
|
|
|
941
1037
|
this.send(client, event);
|
|
942
1038
|
});
|
|
943
1039
|
}
|
|
1040
|
+
/**
|
|
1041
|
+
* 过滤 dev server 产物目录,避免 npm run dev 之类的启动副作用误触发 Refresh。
|
|
1042
|
+
*/
|
|
1043
|
+
shouldIgnoreRepoChange(filename) {
|
|
1044
|
+
if (!filename) return false;
|
|
1045
|
+
const normalizedPath = String(filename).replaceAll("\\", "/").replace(/^\.\//, "");
|
|
1046
|
+
return IGNORED_REPO_PATH_PREFIXES.some((prefix) => normalizedPath === prefix || normalizedPath.startsWith(`${prefix}/`));
|
|
1047
|
+
}
|
|
944
1048
|
send(res, event) {
|
|
945
1049
|
res.write(`data: ${JSON.stringify(event)}
|
|
946
1050
|
|
|
@@ -1107,8 +1211,6 @@ async function startServer(state, port = 4966) {
|
|
|
1107
1211
|
res.status(400).json({ error: "Comment file is not present in the current diff" });
|
|
1108
1212
|
return;
|
|
1109
1213
|
}
|
|
1110
|
-
const store = await readComments(state.session.repoRoot);
|
|
1111
|
-
const existingThread = store.threads.find((thread2) => thread2.fileSnapshotHash === file.snapshotHash && sameAnchor(thread2.anchor, body.anchor));
|
|
1112
1214
|
const comment = {
|
|
1113
1215
|
id: crypto.randomUUID(),
|
|
1114
1216
|
body: commentBody,
|
|
@@ -1116,27 +1218,28 @@ async function startServer(state, port = 4966) {
|
|
|
1116
1218
|
createdAt: now,
|
|
1117
1219
|
updatedAt: now
|
|
1118
1220
|
};
|
|
1119
|
-
|
|
1120
|
-
existingThread.
|
|
1121
|
-
|
|
1122
|
-
|
|
1123
|
-
|
|
1124
|
-
|
|
1125
|
-
|
|
1126
|
-
|
|
1127
|
-
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
|
|
1133
|
-
|
|
1134
|
-
|
|
1135
|
-
|
|
1136
|
-
|
|
1137
|
-
|
|
1138
|
-
|
|
1139
|
-
|
|
1221
|
+
const thread = await updateComments(state.session.repoRoot, (store) => {
|
|
1222
|
+
const existingThread = store.threads.find((item) => item.fileSnapshotHash === file.snapshotHash && sameAnchor(item.anchor, body.anchor));
|
|
1223
|
+
if (existingThread) {
|
|
1224
|
+
existingThread.comments.push(comment);
|
|
1225
|
+
existingThread.status = getOpenThreadStatus(existingThread);
|
|
1226
|
+
existingThread.updatedAt = now;
|
|
1227
|
+
return { changed: true, result: existingThread };
|
|
1228
|
+
}
|
|
1229
|
+
const nextThread = {
|
|
1230
|
+
id: crypto.randomUUID(),
|
|
1231
|
+
filePath: body.filePath,
|
|
1232
|
+
anchor: body.anchor,
|
|
1233
|
+
diffHash: state.session.diffHash,
|
|
1234
|
+
fileSnapshotHash: file.snapshotHash,
|
|
1235
|
+
status: "submit",
|
|
1236
|
+
comments: [comment],
|
|
1237
|
+
createdAt: now,
|
|
1238
|
+
updatedAt: now
|
|
1239
|
+
};
|
|
1240
|
+
store.threads.push(nextThread);
|
|
1241
|
+
return { changed: true, result: nextThread };
|
|
1242
|
+
});
|
|
1140
1243
|
res.status(201).json(thread);
|
|
1141
1244
|
} catch (error) {
|
|
1142
1245
|
next(error);
|
|
@@ -1145,12 +1248,6 @@ async function startServer(state, port = 4966) {
|
|
|
1145
1248
|
app.post("/api/threads/:id/comments", async (req, res, next) => {
|
|
1146
1249
|
try {
|
|
1147
1250
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
1148
|
-
const store = await readComments(state.session.repoRoot);
|
|
1149
|
-
const thread = store.threads.find((item) => item.id === req.params.id);
|
|
1150
|
-
if (!thread) {
|
|
1151
|
-
res.status(404).json({ error: "Thread not found" });
|
|
1152
|
-
return;
|
|
1153
|
-
}
|
|
1154
1251
|
const body = req.body;
|
|
1155
1252
|
const commentBody = body.body?.trim();
|
|
1156
1253
|
if (!commentBody) {
|
|
@@ -1164,40 +1261,59 @@ async function startServer(state, port = 4966) {
|
|
|
1164
1261
|
createdAt: now,
|
|
1165
1262
|
updatedAt: now
|
|
1166
1263
|
};
|
|
1167
|
-
|
|
1168
|
-
|
|
1169
|
-
|
|
1170
|
-
|
|
1171
|
-
|
|
1172
|
-
|
|
1173
|
-
|
|
1264
|
+
const commentResult = await updateComments(state.session.repoRoot, (store) => {
|
|
1265
|
+
const thread = store.threads.find((item) => item.id === req.params.id);
|
|
1266
|
+
if (!thread) {
|
|
1267
|
+
throw new Error("THREAD_NOT_FOUND");
|
|
1268
|
+
}
|
|
1269
|
+
thread.comments.push(comment);
|
|
1270
|
+
if (thread.status !== "resolved") {
|
|
1271
|
+
thread.status = getOpenThreadStatus(thread);
|
|
1272
|
+
}
|
|
1273
|
+
thread.updatedAt = now;
|
|
1274
|
+
return { changed: true, result: comment };
|
|
1275
|
+
});
|
|
1276
|
+
res.status(201).json(commentResult);
|
|
1174
1277
|
} catch (error) {
|
|
1278
|
+
if (error instanceof Error && error.message === "THREAD_NOT_FOUND") {
|
|
1279
|
+
res.status(404).json({ error: "Thread not found" });
|
|
1280
|
+
return;
|
|
1281
|
+
}
|
|
1175
1282
|
next(error);
|
|
1176
1283
|
}
|
|
1177
1284
|
});
|
|
1178
1285
|
app.patch("/api/threads/:id", async (req, res, next) => {
|
|
1179
1286
|
try {
|
|
1180
|
-
const
|
|
1181
|
-
|
|
1182
|
-
|
|
1287
|
+
const thread = await updateComments(state.session.repoRoot, (store) => {
|
|
1288
|
+
const currentThread = store.threads.find((item) => item.id === req.params.id);
|
|
1289
|
+
if (!currentThread) {
|
|
1290
|
+
throw new Error("THREAD_NOT_FOUND");
|
|
1291
|
+
}
|
|
1292
|
+
let changed = false;
|
|
1293
|
+
if (req.body.status === "resolved" || req.body.status === "replied" || req.body.status === "submit") {
|
|
1294
|
+
currentThread.status = req.body.status === "resolved" ? "resolved" : getOpenThreadStatus(currentThread);
|
|
1295
|
+
currentThread.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
1296
|
+
changed = true;
|
|
1297
|
+
}
|
|
1298
|
+
return { changed, result: currentThread };
|
|
1299
|
+
});
|
|
1300
|
+
res.json(thread);
|
|
1301
|
+
} catch (error) {
|
|
1302
|
+
if (error instanceof Error && error.message === "THREAD_NOT_FOUND") {
|
|
1183
1303
|
res.status(404).json({ error: "Thread not found" });
|
|
1184
1304
|
return;
|
|
1185
1305
|
}
|
|
1186
|
-
if (req.body.status === "resolved" || req.body.status === "replied" || req.body.status === "submit") {
|
|
1187
|
-
thread.status = req.body.status === "resolved" ? "resolved" : getOpenThreadStatus(thread);
|
|
1188
|
-
thread.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
1189
|
-
}
|
|
1190
|
-
await writeComments(state.session.repoRoot, store);
|
|
1191
|
-
res.json(thread);
|
|
1192
|
-
} catch (error) {
|
|
1193
1306
|
next(error);
|
|
1194
1307
|
}
|
|
1195
1308
|
});
|
|
1196
1309
|
app.delete("/api/threads/:id", async (req, res, next) => {
|
|
1197
1310
|
try {
|
|
1198
|
-
|
|
1199
|
-
|
|
1200
|
-
|
|
1311
|
+
await updateComments(state.session.repoRoot, (store) => {
|
|
1312
|
+
const nextThreads = store.threads.filter((item) => item.id !== req.params.id);
|
|
1313
|
+
const changed = nextThreads.length !== store.threads.length;
|
|
1314
|
+
store.threads = nextThreads;
|
|
1315
|
+
return { changed, result: void 0 };
|
|
1316
|
+
});
|
|
1201
1317
|
res.status(204).end();
|
|
1202
1318
|
} catch (error) {
|
|
1203
1319
|
next(error);
|
|
@@ -1206,67 +1322,93 @@ async function startServer(state, port = 4966) {
|
|
|
1206
1322
|
app.patch("/api/threads/:id/comments/:commentId", async (req, res, next) => {
|
|
1207
1323
|
try {
|
|
1208
1324
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
1209
|
-
const
|
|
1210
|
-
|
|
1211
|
-
|
|
1325
|
+
const nextBody = String(req.body?.body ?? "").trim();
|
|
1326
|
+
if (!nextBody) {
|
|
1327
|
+
res.status(400).json({ error: "Comment body is required" });
|
|
1328
|
+
return;
|
|
1329
|
+
}
|
|
1330
|
+
const comment = await updateComments(state.session.repoRoot, (store) => {
|
|
1331
|
+
const thread = store.threads.find((item) => item.id === req.params.id);
|
|
1332
|
+
if (!thread) {
|
|
1333
|
+
throw new Error("THREAD_NOT_FOUND");
|
|
1334
|
+
}
|
|
1335
|
+
if (getThreadStatus(thread) !== "submit") {
|
|
1336
|
+
throw new Error("THREAD_NOT_EDITABLE");
|
|
1337
|
+
}
|
|
1338
|
+
const currentComment = thread.comments.find((item) => item.id === req.params.commentId);
|
|
1339
|
+
if (!currentComment) {
|
|
1340
|
+
throw new Error("COMMENT_NOT_FOUND");
|
|
1341
|
+
}
|
|
1342
|
+
if (currentComment.author === "agent") {
|
|
1343
|
+
throw new Error("AGENT_COMMENT_READ_ONLY");
|
|
1344
|
+
}
|
|
1345
|
+
currentComment.body = nextBody;
|
|
1346
|
+
currentComment.updatedAt = now;
|
|
1347
|
+
thread.updatedAt = now;
|
|
1348
|
+
return { changed: true, result: currentComment };
|
|
1349
|
+
});
|
|
1350
|
+
res.json(comment);
|
|
1351
|
+
} catch (error) {
|
|
1352
|
+
if (error instanceof Error && error.message === "THREAD_NOT_FOUND") {
|
|
1212
1353
|
res.status(404).json({ error: "Thread not found" });
|
|
1213
1354
|
return;
|
|
1214
1355
|
}
|
|
1215
|
-
if (
|
|
1356
|
+
if (error instanceof Error && error.message === "THREAD_NOT_EDITABLE") {
|
|
1216
1357
|
res.status(400).json({ error: "Only submitted comments can be edited" });
|
|
1217
1358
|
return;
|
|
1218
1359
|
}
|
|
1219
|
-
|
|
1220
|
-
if (!comment) {
|
|
1360
|
+
if (error instanceof Error && error.message === "COMMENT_NOT_FOUND") {
|
|
1221
1361
|
res.status(404).json({ error: "Comment not found" });
|
|
1222
1362
|
return;
|
|
1223
1363
|
}
|
|
1224
|
-
if (
|
|
1364
|
+
if (error instanceof Error && error.message === "AGENT_COMMENT_READ_ONLY") {
|
|
1225
1365
|
res.status(400).json({ error: "Agent comments are read-only" });
|
|
1226
1366
|
return;
|
|
1227
1367
|
}
|
|
1228
|
-
const nextBody = String(req.body?.body ?? "").trim();
|
|
1229
|
-
if (!nextBody) {
|
|
1230
|
-
res.status(400).json({ error: "Comment body is required" });
|
|
1231
|
-
return;
|
|
1232
|
-
}
|
|
1233
|
-
comment.body = nextBody;
|
|
1234
|
-
comment.updatedAt = now;
|
|
1235
|
-
thread.updatedAt = now;
|
|
1236
|
-
await writeComments(state.session.repoRoot, store);
|
|
1237
|
-
res.json(comment);
|
|
1238
|
-
} catch (error) {
|
|
1239
1368
|
next(error);
|
|
1240
1369
|
}
|
|
1241
1370
|
});
|
|
1242
1371
|
app.delete("/api/threads/:id/comments/:commentId", async (req, res, next) => {
|
|
1243
1372
|
try {
|
|
1244
1373
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
1245
|
-
|
|
1246
|
-
|
|
1247
|
-
|
|
1374
|
+
await updateComments(state.session.repoRoot, (store) => {
|
|
1375
|
+
const thread = store.threads.find((item) => item.id === req.params.id);
|
|
1376
|
+
if (!thread) {
|
|
1377
|
+
throw new Error("THREAD_NOT_FOUND");
|
|
1378
|
+
}
|
|
1379
|
+
if (getThreadStatus(thread) !== "submit") {
|
|
1380
|
+
throw new Error("THREAD_NOT_EDITABLE");
|
|
1381
|
+
}
|
|
1382
|
+
const comment = thread.comments.find((item) => item.id === req.params.commentId);
|
|
1383
|
+
if (!comment) {
|
|
1384
|
+
throw new Error("COMMENT_NOT_FOUND");
|
|
1385
|
+
}
|
|
1386
|
+
if (comment.author === "agent") {
|
|
1387
|
+
throw new Error("AGENT_COMMENT_READ_ONLY");
|
|
1388
|
+
}
|
|
1389
|
+
thread.comments = thread.comments.filter((item) => item.id !== req.params.commentId);
|
|
1390
|
+
thread.updatedAt = now;
|
|
1391
|
+
store.threads = store.threads.filter((item) => item.id !== thread.id || thread.comments.length > 0);
|
|
1392
|
+
return { changed: true, result: void 0 };
|
|
1393
|
+
});
|
|
1394
|
+
res.status(204).end();
|
|
1395
|
+
} catch (error) {
|
|
1396
|
+
if (error instanceof Error && error.message === "THREAD_NOT_FOUND") {
|
|
1248
1397
|
res.status(404).json({ error: "Thread not found" });
|
|
1249
1398
|
return;
|
|
1250
1399
|
}
|
|
1251
|
-
if (
|
|
1400
|
+
if (error instanceof Error && error.message === "THREAD_NOT_EDITABLE") {
|
|
1252
1401
|
res.status(400).json({ error: "Only submitted comments can be deleted" });
|
|
1253
1402
|
return;
|
|
1254
1403
|
}
|
|
1255
|
-
|
|
1256
|
-
if (!comment) {
|
|
1404
|
+
if (error instanceof Error && error.message === "COMMENT_NOT_FOUND") {
|
|
1257
1405
|
res.status(404).json({ error: "Comment not found" });
|
|
1258
1406
|
return;
|
|
1259
1407
|
}
|
|
1260
|
-
if (
|
|
1408
|
+
if (error instanceof Error && error.message === "AGENT_COMMENT_READ_ONLY") {
|
|
1261
1409
|
res.status(400).json({ error: "Agent comments are read-only" });
|
|
1262
1410
|
return;
|
|
1263
1411
|
}
|
|
1264
|
-
thread.comments = thread.comments.filter((item) => item.id !== req.params.commentId);
|
|
1265
|
-
thread.updatedAt = now;
|
|
1266
|
-
store.threads = store.threads.filter((item) => item.id !== thread.id || thread.comments.length > 0);
|
|
1267
|
-
await writeComments(state.session.repoRoot, store);
|
|
1268
|
-
res.status(204).end();
|
|
1269
|
-
} catch (error) {
|
|
1270
1412
|
next(error);
|
|
1271
1413
|
}
|
|
1272
1414
|
});
|