diffprism 0.27.0 → 0.28.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -764,20 +764,222 @@ function analyze(diffSet) {
764
764
  };
765
765
  }
766
766
 
767
+ // packages/core/src/watch-file.ts
768
+ import fs from "fs";
769
+ import path3 from "path";
770
+ import { execSync as execSync2 } from "child_process";
771
+ function findGitRoot(cwd) {
772
+ const root = execSync2("git rev-parse --show-toplevel", {
773
+ cwd: cwd ?? process.cwd(),
774
+ encoding: "utf-8"
775
+ }).trim();
776
+ return root;
777
+ }
778
+ function watchFilePath(cwd) {
779
+ const gitRoot = findGitRoot(cwd);
780
+ return path3.join(gitRoot, ".diffprism", "watch.json");
781
+ }
782
+ function isPidAlive(pid) {
783
+ try {
784
+ process.kill(pid, 0);
785
+ return true;
786
+ } catch {
787
+ return false;
788
+ }
789
+ }
790
+ function writeWatchFile(cwd, info) {
791
+ const filePath = watchFilePath(cwd);
792
+ const dir = path3.dirname(filePath);
793
+ if (!fs.existsSync(dir)) {
794
+ fs.mkdirSync(dir, { recursive: true });
795
+ }
796
+ fs.writeFileSync(filePath, JSON.stringify(info, null, 2) + "\n");
797
+ }
798
+ function readWatchFile(cwd) {
799
+ const filePath = watchFilePath(cwd);
800
+ if (!fs.existsSync(filePath)) {
801
+ return null;
802
+ }
803
+ try {
804
+ const raw = fs.readFileSync(filePath, "utf-8");
805
+ const info = JSON.parse(raw);
806
+ if (!isPidAlive(info.pid)) {
807
+ fs.unlinkSync(filePath);
808
+ return null;
809
+ }
810
+ return info;
811
+ } catch {
812
+ return null;
813
+ }
814
+ }
815
+ function removeWatchFile(cwd) {
816
+ try {
817
+ const filePath = watchFilePath(cwd);
818
+ if (fs.existsSync(filePath)) {
819
+ fs.unlinkSync(filePath);
820
+ }
821
+ } catch {
822
+ }
823
+ }
824
+ function reviewResultPath(cwd) {
825
+ const gitRoot = findGitRoot(cwd);
826
+ return path3.join(gitRoot, ".diffprism", "last-review.json");
827
+ }
828
+ function writeReviewResult(cwd, result) {
829
+ const filePath = reviewResultPath(cwd);
830
+ const dir = path3.dirname(filePath);
831
+ if (!fs.existsSync(dir)) {
832
+ fs.mkdirSync(dir, { recursive: true });
833
+ }
834
+ const data = {
835
+ result,
836
+ timestamp: Date.now(),
837
+ consumed: false
838
+ };
839
+ fs.writeFileSync(filePath, JSON.stringify(data, null, 2) + "\n");
840
+ }
841
+ function readReviewResult(cwd) {
842
+ try {
843
+ const filePath = reviewResultPath(cwd);
844
+ if (!fs.existsSync(filePath)) {
845
+ return null;
846
+ }
847
+ const raw = fs.readFileSync(filePath, "utf-8");
848
+ const data = JSON.parse(raw);
849
+ if (data.consumed) {
850
+ return null;
851
+ }
852
+ return data;
853
+ } catch {
854
+ return null;
855
+ }
856
+ }
857
+ function consumeReviewResult(cwd) {
858
+ try {
859
+ const filePath = reviewResultPath(cwd);
860
+ if (!fs.existsSync(filePath)) {
861
+ return;
862
+ }
863
+ const raw = fs.readFileSync(filePath, "utf-8");
864
+ const data = JSON.parse(raw);
865
+ data.consumed = true;
866
+ fs.writeFileSync(filePath, JSON.stringify(data, null, 2) + "\n");
867
+ } catch {
868
+ }
869
+ }
870
+
767
871
  // packages/core/src/pipeline.ts
768
872
  import getPort from "get-port";
769
873
  import open from "open";
770
874
 
771
- // packages/core/src/ws-bridge.ts
875
+ // packages/core/src/watch-bridge.ts
876
+ import http from "http";
772
877
  import { WebSocketServer, WebSocket } from "ws";
773
- function createWsBridge(port) {
774
- const wss2 = new WebSocketServer({ port });
878
+ function createWatchBridge(port, callbacks) {
775
879
  let client = null;
776
- let resultResolve = null;
777
- let resultReject = null;
778
- let pendingInit = null;
779
880
  let initPayload = null;
881
+ let pendingInit = null;
780
882
  let closeTimer = null;
883
+ let submitCallback = null;
884
+ let resultReject = null;
885
+ const httpServer = http.createServer(async (req, res) => {
886
+ res.setHeader("Access-Control-Allow-Origin", "*");
887
+ res.setHeader("Access-Control-Allow-Methods", "GET, POST, OPTIONS");
888
+ res.setHeader("Access-Control-Allow-Headers", "Content-Type");
889
+ if (req.method === "OPTIONS") {
890
+ res.writeHead(204);
891
+ res.end();
892
+ return;
893
+ }
894
+ if (req.method === "GET" && req.url === "/api/status") {
895
+ res.writeHead(200, { "Content-Type": "application/json" });
896
+ res.end(JSON.stringify({ running: true, pid: process.pid }));
897
+ return;
898
+ }
899
+ if (req.method === "POST" && req.url === "/api/context") {
900
+ let body = "";
901
+ req.on("data", (chunk) => {
902
+ body += chunk.toString();
903
+ });
904
+ req.on("end", () => {
905
+ try {
906
+ const payload = JSON.parse(body);
907
+ callbacks.onContextUpdate(payload);
908
+ sendToClient({ type: "context:update", payload });
909
+ res.writeHead(200, { "Content-Type": "application/json" });
910
+ res.end(JSON.stringify({ ok: true }));
911
+ } catch {
912
+ res.writeHead(400, { "Content-Type": "application/json" });
913
+ res.end(JSON.stringify({ error: "Invalid JSON" }));
914
+ }
915
+ });
916
+ return;
917
+ }
918
+ if (req.method === "POST" && req.url === "/api/refresh") {
919
+ callbacks.onRefreshRequest();
920
+ res.writeHead(200, { "Content-Type": "application/json" });
921
+ res.end(JSON.stringify({ ok: true }));
922
+ return;
923
+ }
924
+ const pathname = (req.url ?? "").split("?")[0];
925
+ if (req.method === "GET" && (pathname === "/api/refs" || /^\/api\/reviews\/[^/]+\/refs$/.test(pathname))) {
926
+ if (callbacks.onRefsRequest) {
927
+ const refsPayload = await callbacks.onRefsRequest();
928
+ if (refsPayload) {
929
+ res.writeHead(200, { "Content-Type": "application/json" });
930
+ res.end(JSON.stringify(refsPayload));
931
+ } else {
932
+ res.writeHead(500, { "Content-Type": "application/json" });
933
+ res.end(JSON.stringify({ error: "Failed to list git refs" }));
934
+ }
935
+ } else {
936
+ res.writeHead(404);
937
+ res.end("Not found");
938
+ }
939
+ return;
940
+ }
941
+ if (req.method === "POST" && (pathname === "/api/compare" || /^\/api\/reviews\/[^/]+\/compare$/.test(pathname))) {
942
+ if (callbacks.onCompareRequest) {
943
+ let body = "";
944
+ req.on("data", (chunk) => {
945
+ body += chunk.toString();
946
+ });
947
+ req.on("end", async () => {
948
+ try {
949
+ const { ref } = JSON.parse(body);
950
+ if (!ref) {
951
+ res.writeHead(400, { "Content-Type": "application/json" });
952
+ res.end(JSON.stringify({ error: "Missing ref" }));
953
+ return;
954
+ }
955
+ const success = await callbacks.onCompareRequest(ref);
956
+ if (success) {
957
+ res.writeHead(200, { "Content-Type": "application/json" });
958
+ res.end(JSON.stringify({ ok: true }));
959
+ } else {
960
+ res.writeHead(400, { "Content-Type": "application/json" });
961
+ res.end(JSON.stringify({ error: "Failed to compute diff" }));
962
+ }
963
+ } catch {
964
+ res.writeHead(400, { "Content-Type": "application/json" });
965
+ res.end(JSON.stringify({ error: "Invalid JSON" }));
966
+ }
967
+ });
968
+ } else {
969
+ res.writeHead(404);
970
+ res.end("Not found");
971
+ }
972
+ return;
973
+ }
974
+ res.writeHead(404);
975
+ res.end("Not found");
976
+ });
977
+ const wss2 = new WebSocketServer({ server: httpServer });
978
+ function sendToClient(msg) {
979
+ if (client && client.readyState === WebSocket.OPEN) {
980
+ client.send(JSON.stringify(msg));
981
+ }
982
+ }
781
983
  wss2.on("connection", (ws) => {
782
984
  if (closeTimer) {
783
985
  clearTimeout(closeTimer);
@@ -786,20 +988,16 @@ function createWsBridge(port) {
786
988
  client = ws;
787
989
  const payload = pendingInit ?? initPayload;
788
990
  if (payload) {
789
- const msg = {
790
- type: "review:init",
791
- payload
792
- };
793
- ws.send(JSON.stringify(msg));
991
+ sendToClient({ type: "review:init", payload });
794
992
  pendingInit = null;
795
993
  }
796
994
  ws.on("message", (data) => {
797
995
  try {
798
996
  const msg = JSON.parse(data.toString());
799
- if (msg.type === "review:submit" && resultResolve) {
800
- resultResolve(msg.payload);
801
- resultResolve = null;
802
- resultReject = null;
997
+ if (msg.type === "review:submit" && submitCallback) {
998
+ submitCallback(msg.payload);
999
+ } else if (msg.type === "diff:change_ref" && callbacks.onDiffRefChange) {
1000
+ callbacks.onDiffRefChange(msg.payload.diffRef);
803
1001
  }
804
1002
  } catch {
805
1003
  }
@@ -808,40 +1006,168 @@ function createWsBridge(port) {
808
1006
  client = null;
809
1007
  if (resultReject) {
810
1008
  closeTimer = setTimeout(() => {
1009
+ closeTimer = null;
811
1010
  if (resultReject) {
812
1011
  resultReject(new Error("Browser closed before review was submitted"));
813
- resultResolve = null;
814
1012
  resultReject = null;
1013
+ submitCallback = null;
815
1014
  }
816
1015
  }, 2e3);
1016
+ } else {
1017
+ closeTimer = setTimeout(() => {
1018
+ closeTimer = null;
1019
+ }, 2e3);
817
1020
  }
818
1021
  });
819
1022
  });
820
- return {
821
- port,
822
- sendInit(payload) {
823
- initPayload = payload;
824
- if (client && client.readyState === WebSocket.OPEN) {
825
- const msg = {
826
- type: "review:init",
827
- payload
1023
+ return new Promise((resolve, reject) => {
1024
+ httpServer.on("error", reject);
1025
+ httpServer.listen(port, () => {
1026
+ resolve({
1027
+ port,
1028
+ sendInit(payload) {
1029
+ initPayload = payload;
1030
+ if (client && client.readyState === WebSocket.OPEN) {
1031
+ sendToClient({ type: "review:init", payload });
1032
+ } else {
1033
+ pendingInit = payload;
1034
+ }
1035
+ },
1036
+ storeInitPayload(payload) {
1037
+ initPayload = payload;
1038
+ },
1039
+ sendDiffUpdate(payload) {
1040
+ sendToClient({ type: "diff:update", payload });
1041
+ },
1042
+ sendContextUpdate(payload) {
1043
+ sendToClient({ type: "context:update", payload });
1044
+ },
1045
+ sendDiffError(payload) {
1046
+ sendToClient({ type: "diff:error", payload });
1047
+ },
1048
+ onSubmit(callback) {
1049
+ submitCallback = callback;
1050
+ },
1051
+ waitForResult() {
1052
+ return new Promise((resolve2, reject2) => {
1053
+ submitCallback = resolve2;
1054
+ resultReject = reject2;
1055
+ });
1056
+ },
1057
+ triggerRefresh() {
1058
+ callbacks.onRefreshRequest();
1059
+ },
1060
+ async close() {
1061
+ if (closeTimer) {
1062
+ clearTimeout(closeTimer);
1063
+ }
1064
+ for (const ws of wss2.clients) {
1065
+ ws.close();
1066
+ }
1067
+ wss2.close();
1068
+ await new Promise((resolve2) => {
1069
+ httpServer.close(() => resolve2());
1070
+ });
1071
+ }
1072
+ });
1073
+ });
1074
+ });
1075
+ }
1076
+
1077
+ // packages/core/src/diff-utils.ts
1078
+ import { createHash } from "crypto";
1079
+ function hashDiff(rawDiff) {
1080
+ return createHash("sha256").update(rawDiff).digest("hex");
1081
+ }
1082
+ function fileKey(file) {
1083
+ return file.stage ? `${file.stage}:${file.path}` : file.path;
1084
+ }
1085
+ function detectChangedFiles(oldDiffSet, newDiffSet) {
1086
+ if (!oldDiffSet) {
1087
+ return newDiffSet.files.map(fileKey);
1088
+ }
1089
+ const oldFiles = new Map(
1090
+ oldDiffSet.files.map((f) => [fileKey(f), f])
1091
+ );
1092
+ const changed = [];
1093
+ for (const newFile of newDiffSet.files) {
1094
+ const key = fileKey(newFile);
1095
+ const oldFile = oldFiles.get(key);
1096
+ if (!oldFile) {
1097
+ changed.push(key);
1098
+ } else if (oldFile.additions !== newFile.additions || oldFile.deletions !== newFile.deletions) {
1099
+ changed.push(key);
1100
+ }
1101
+ }
1102
+ for (const oldFile of oldDiffSet.files) {
1103
+ if (!newDiffSet.files.some((f) => fileKey(f) === fileKey(oldFile))) {
1104
+ changed.push(fileKey(oldFile));
1105
+ }
1106
+ }
1107
+ return changed;
1108
+ }
1109
+
1110
+ // packages/core/src/diff-poller.ts
1111
+ function createDiffPoller(options) {
1112
+ let { diffRef } = options;
1113
+ const { cwd, pollInterval, onDiffChanged, onError, silent } = options;
1114
+ let lastDiffHash = null;
1115
+ let lastDiffSet = null;
1116
+ let refreshRequested = false;
1117
+ let interval = null;
1118
+ let running = false;
1119
+ function poll() {
1120
+ if (!running) return;
1121
+ try {
1122
+ const { diffSet: newDiffSet, rawDiff: newRawDiff } = getDiff(diffRef, { cwd });
1123
+ const newHash = hashDiff(newRawDiff);
1124
+ if (newHash !== lastDiffHash || refreshRequested) {
1125
+ refreshRequested = false;
1126
+ const newBriefing = analyze(newDiffSet);
1127
+ const changedFiles = detectChangedFiles(lastDiffSet, newDiffSet);
1128
+ lastDiffHash = newHash;
1129
+ lastDiffSet = newDiffSet;
1130
+ const updatePayload = {
1131
+ diffSet: newDiffSet,
1132
+ rawDiff: newRawDiff,
1133
+ briefing: newBriefing,
1134
+ changedFiles,
1135
+ timestamp: Date.now()
828
1136
  };
829
- client.send(JSON.stringify(msg));
830
- } else {
831
- pendingInit = payload;
1137
+ onDiffChanged(updatePayload);
832
1138
  }
1139
+ } catch (err) {
1140
+ if (onError && err instanceof Error) {
1141
+ onError(err);
1142
+ }
1143
+ }
1144
+ }
1145
+ return {
1146
+ start() {
1147
+ if (running) return;
1148
+ running = true;
1149
+ try {
1150
+ const { diffSet: initialDiffSet, rawDiff: initialRawDiff } = getDiff(diffRef, { cwd });
1151
+ lastDiffHash = hashDiff(initialRawDiff);
1152
+ lastDiffSet = initialDiffSet;
1153
+ } catch {
1154
+ }
1155
+ interval = setInterval(poll, pollInterval);
833
1156
  },
834
- waitForResult() {
835
- return new Promise((resolve, reject) => {
836
- resultResolve = resolve;
837
- resultReject = reject;
838
- });
839
- },
840
- close() {
841
- for (const ws of wss2.clients) {
842
- ws.close();
1157
+ stop() {
1158
+ running = false;
1159
+ if (interval) {
1160
+ clearInterval(interval);
1161
+ interval = null;
843
1162
  }
844
- wss2.close();
1163
+ },
1164
+ setDiffRef(newRef) {
1165
+ diffRef = newRef;
1166
+ lastDiffHash = null;
1167
+ lastDiffSet = null;
1168
+ },
1169
+ refresh() {
1170
+ refreshRequested = true;
845
1171
  }
846
1172
  };
847
1173
  }
@@ -868,9 +1194,9 @@ function updateSession(id, update) {
868
1194
  }
869
1195
 
870
1196
  // packages/core/src/ui-server.ts
871
- import http from "http";
872
- import fs from "fs";
873
- import path3 from "path";
1197
+ import http2 from "http";
1198
+ import fs2 from "fs";
1199
+ import path4 from "path";
874
1200
  import { fileURLToPath } from "url";
875
1201
  var MIME_TYPES = {
876
1202
  ".html": "text/html",
@@ -885,14 +1211,14 @@ var MIME_TYPES = {
885
1211
  };
886
1212
  function resolveUiDist() {
887
1213
  const thisFile = fileURLToPath(import.meta.url);
888
- const thisDir = path3.dirname(thisFile);
889
- const publishedUiDist = path3.resolve(thisDir, "..", "ui-dist");
890
- if (fs.existsSync(path3.join(publishedUiDist, "index.html"))) {
1214
+ const thisDir = path4.dirname(thisFile);
1215
+ const publishedUiDist = path4.resolve(thisDir, "..", "ui-dist");
1216
+ if (fs2.existsSync(path4.join(publishedUiDist, "index.html"))) {
891
1217
  return publishedUiDist;
892
1218
  }
893
- const workspaceRoot = path3.resolve(thisDir, "..", "..", "..");
894
- const devUiDist = path3.join(workspaceRoot, "packages", "ui", "dist");
895
- if (fs.existsSync(path3.join(devUiDist, "index.html"))) {
1219
+ const workspaceRoot = path4.resolve(thisDir, "..", "..", "..");
1220
+ const devUiDist = path4.join(workspaceRoot, "packages", "ui", "dist");
1221
+ if (fs2.existsSync(path4.join(devUiDist, "index.html"))) {
896
1222
  return devUiDist;
897
1223
  }
898
1224
  throw new Error(
@@ -901,10 +1227,10 @@ function resolveUiDist() {
901
1227
  }
902
1228
  function resolveUiRoot() {
903
1229
  const thisFile = fileURLToPath(import.meta.url);
904
- const thisDir = path3.dirname(thisFile);
905
- const workspaceRoot = path3.resolve(thisDir, "..", "..", "..");
906
- const uiRoot = path3.join(workspaceRoot, "packages", "ui");
907
- if (fs.existsSync(path3.join(uiRoot, "index.html"))) {
1230
+ const thisDir = path4.dirname(thisFile);
1231
+ const workspaceRoot = path4.resolve(thisDir, "..", "..", "..");
1232
+ const uiRoot = path4.join(workspaceRoot, "packages", "ui");
1233
+ if (fs2.existsSync(path4.join(uiRoot, "index.html"))) {
908
1234
  return uiRoot;
909
1235
  }
910
1236
  throw new Error(
@@ -922,16 +1248,16 @@ async function startViteDevServer(uiRoot, port, silent) {
922
1248
  return vite;
923
1249
  }
924
1250
  function createStaticServer(distPath, port) {
925
- const server = http.createServer((req, res) => {
1251
+ const server = http2.createServer((req, res) => {
926
1252
  const urlPath = req.url?.split("?")[0] ?? "/";
927
- let filePath = path3.join(distPath, urlPath === "/" ? "index.html" : urlPath);
928
- if (!fs.existsSync(filePath)) {
929
- filePath = path3.join(distPath, "index.html");
1253
+ let filePath = path4.join(distPath, urlPath === "/" ? "index.html" : urlPath);
1254
+ if (!fs2.existsSync(filePath)) {
1255
+ filePath = path4.join(distPath, "index.html");
930
1256
  }
931
- const ext = path3.extname(filePath);
1257
+ const ext = path4.extname(filePath);
932
1258
  const contentType = MIME_TYPES[ext] ?? "application/octet-stream";
933
1259
  try {
934
- const content = fs.readFileSync(filePath);
1260
+ const content = fs2.readFileSync(filePath);
935
1261
  res.writeHead(200, { "Content-Type": contentType });
936
1262
  res.end(content);
937
1263
  } catch {
@@ -963,11 +1289,75 @@ async function startReview(options) {
963
1289
  const briefing = analyze(diffSet);
964
1290
  const session = createSession(options);
965
1291
  updateSession(session.id, { status: "in_progress" });
966
- const [wsPort, httpPort] = await Promise.all([
1292
+ const metadata = {
1293
+ title,
1294
+ description,
1295
+ reasoning,
1296
+ currentBranch
1297
+ };
1298
+ let poller = null;
1299
+ const [bridgePort, httpPort] = await Promise.all([
967
1300
  getPort(),
968
1301
  getPort()
969
1302
  ]);
970
- const bridge = createWsBridge(wsPort);
1303
+ function handleDiffRefChange(newRef) {
1304
+ const { diffSet: newDiffSet, rawDiff: newRawDiff } = getDiff(newRef, { cwd });
1305
+ const newBriefing = analyze(newDiffSet);
1306
+ bridge.sendDiffUpdate({
1307
+ diffSet: newDiffSet,
1308
+ rawDiff: newRawDiff,
1309
+ briefing: newBriefing,
1310
+ changedFiles: newDiffSet.files.map((f) => f.path),
1311
+ timestamp: Date.now()
1312
+ });
1313
+ bridge.storeInitPayload({
1314
+ reviewId: session.id,
1315
+ diffSet: newDiffSet,
1316
+ rawDiff: newRawDiff,
1317
+ briefing: newBriefing,
1318
+ metadata,
1319
+ watchMode: true
1320
+ });
1321
+ poller?.setDiffRef(newRef);
1322
+ }
1323
+ const bridge = await createWatchBridge(bridgePort, {
1324
+ onRefreshRequest: () => {
1325
+ poller?.refresh();
1326
+ },
1327
+ onContextUpdate: (payload) => {
1328
+ if (payload.reasoning !== void 0) metadata.reasoning = payload.reasoning;
1329
+ if (payload.title !== void 0) metadata.title = payload.title;
1330
+ if (payload.description !== void 0) metadata.description = payload.description;
1331
+ },
1332
+ onDiffRefChange: (newRef) => {
1333
+ try {
1334
+ handleDiffRefChange(newRef);
1335
+ } catch (err) {
1336
+ bridge.sendDiffError({
1337
+ error: err instanceof Error ? err.message : String(err)
1338
+ });
1339
+ }
1340
+ },
1341
+ onRefsRequest: async () => {
1342
+ try {
1343
+ const resolvedCwd = cwd ?? process.cwd();
1344
+ const branches = listBranches({ cwd: resolvedCwd });
1345
+ const commits = listCommits({ cwd: resolvedCwd });
1346
+ const branch = getCurrentBranch({ cwd: resolvedCwd });
1347
+ return { branches, commits, currentBranch: branch };
1348
+ } catch {
1349
+ return null;
1350
+ }
1351
+ },
1352
+ onCompareRequest: async (ref) => {
1353
+ try {
1354
+ handleDiffRefChange(ref);
1355
+ return true;
1356
+ } catch {
1357
+ return false;
1358
+ }
1359
+ }
1360
+ });
971
1361
  let httpServer = null;
972
1362
  let viteServer = null;
973
1363
  try {
@@ -978,7 +1368,15 @@ async function startReview(options) {
978
1368
  const uiDist = resolveUiDist();
979
1369
  httpServer = await createStaticServer(uiDist, httpPort);
980
1370
  }
981
- const url = `http://localhost:${httpPort}?wsPort=${wsPort}&reviewId=${session.id}`;
1371
+ writeWatchFile(cwd, {
1372
+ wsPort: bridgePort,
1373
+ uiPort: httpPort,
1374
+ pid: process.pid,
1375
+ cwd: cwd ?? process.cwd(),
1376
+ diffRef,
1377
+ startedAt: Date.now()
1378
+ });
1379
+ const url = `http://localhost:${httpPort}?wsPort=${bridgePort}&httpPort=${bridgePort}&reviewId=${session.id}`;
982
1380
  if (!silent) {
983
1381
  console.log(`
984
1382
  DiffPrism Review: ${title ?? briefing.summary}`);
@@ -991,124 +1389,46 @@ DiffPrism Review: ${title ?? briefing.summary}`);
991
1389
  diffSet,
992
1390
  rawDiff,
993
1391
  briefing,
994
- metadata: { title, description, reasoning, currentBranch }
1392
+ metadata,
1393
+ watchMode: true
995
1394
  };
996
1395
  bridge.sendInit(initPayload);
1396
+ poller = createDiffPoller({
1397
+ diffRef,
1398
+ cwd: cwd ?? process.cwd(),
1399
+ pollInterval: 1e3,
1400
+ onDiffChanged: (updatePayload) => {
1401
+ bridge.storeInitPayload({
1402
+ reviewId: session.id,
1403
+ diffSet: updatePayload.diffSet,
1404
+ rawDiff: updatePayload.rawDiff,
1405
+ briefing: updatePayload.briefing,
1406
+ metadata,
1407
+ watchMode: true
1408
+ });
1409
+ bridge.sendDiffUpdate(updatePayload);
1410
+ if (!silent && updatePayload.changedFiles.length > 0) {
1411
+ console.log(
1412
+ `[${(/* @__PURE__ */ new Date()).toLocaleTimeString()}] Diff updated: ${updatePayload.changedFiles.length} file(s) changed`
1413
+ );
1414
+ }
1415
+ }
1416
+ });
1417
+ poller.start();
997
1418
  const result = await bridge.waitForResult();
1419
+ poller.stop();
998
1420
  updateSession(session.id, { status: "completed", result });
999
1421
  return result;
1000
1422
  } finally {
1001
- bridge.close();
1423
+ poller?.stop();
1424
+ await bridge.close();
1425
+ removeWatchFile(cwd);
1002
1426
  if (viteServer) {
1003
- await viteServer.close();
1004
- }
1005
- if (httpServer) {
1006
- httpServer.close();
1007
- }
1008
- }
1009
- }
1010
-
1011
- // packages/core/src/watch-file.ts
1012
- import fs2 from "fs";
1013
- import path4 from "path";
1014
- import { execSync as execSync2 } from "child_process";
1015
- function findGitRoot(cwd) {
1016
- const root = execSync2("git rev-parse --show-toplevel", {
1017
- cwd: cwd ?? process.cwd(),
1018
- encoding: "utf-8"
1019
- }).trim();
1020
- return root;
1021
- }
1022
- function watchFilePath(cwd) {
1023
- const gitRoot = findGitRoot(cwd);
1024
- return path4.join(gitRoot, ".diffprism", "watch.json");
1025
- }
1026
- function isPidAlive(pid) {
1027
- try {
1028
- process.kill(pid, 0);
1029
- return true;
1030
- } catch {
1031
- return false;
1032
- }
1033
- }
1034
- function writeWatchFile(cwd, info) {
1035
- const filePath = watchFilePath(cwd);
1036
- const dir = path4.dirname(filePath);
1037
- if (!fs2.existsSync(dir)) {
1038
- fs2.mkdirSync(dir, { recursive: true });
1039
- }
1040
- fs2.writeFileSync(filePath, JSON.stringify(info, null, 2) + "\n");
1041
- }
1042
- function readWatchFile(cwd) {
1043
- const filePath = watchFilePath(cwd);
1044
- if (!fs2.existsSync(filePath)) {
1045
- return null;
1046
- }
1047
- try {
1048
- const raw = fs2.readFileSync(filePath, "utf-8");
1049
- const info = JSON.parse(raw);
1050
- if (!isPidAlive(info.pid)) {
1051
- fs2.unlinkSync(filePath);
1052
- return null;
1053
- }
1054
- return info;
1055
- } catch {
1056
- return null;
1057
- }
1058
- }
1059
- function removeWatchFile(cwd) {
1060
- try {
1061
- const filePath = watchFilePath(cwd);
1062
- if (fs2.existsSync(filePath)) {
1063
- fs2.unlinkSync(filePath);
1064
- }
1065
- } catch {
1066
- }
1067
- }
1068
- function reviewResultPath(cwd) {
1069
- const gitRoot = findGitRoot(cwd);
1070
- return path4.join(gitRoot, ".diffprism", "last-review.json");
1071
- }
1072
- function writeReviewResult(cwd, result) {
1073
- const filePath = reviewResultPath(cwd);
1074
- const dir = path4.dirname(filePath);
1075
- if (!fs2.existsSync(dir)) {
1076
- fs2.mkdirSync(dir, { recursive: true });
1077
- }
1078
- const data = {
1079
- result,
1080
- timestamp: Date.now(),
1081
- consumed: false
1082
- };
1083
- fs2.writeFileSync(filePath, JSON.stringify(data, null, 2) + "\n");
1084
- }
1085
- function readReviewResult(cwd) {
1086
- try {
1087
- const filePath = reviewResultPath(cwd);
1088
- if (!fs2.existsSync(filePath)) {
1089
- return null;
1090
- }
1091
- const raw = fs2.readFileSync(filePath, "utf-8");
1092
- const data = JSON.parse(raw);
1093
- if (data.consumed) {
1094
- return null;
1427
+ await viteServer.close();
1095
1428
  }
1096
- return data;
1097
- } catch {
1098
- return null;
1099
- }
1100
- }
1101
- function consumeReviewResult(cwd) {
1102
- try {
1103
- const filePath = reviewResultPath(cwd);
1104
- if (!fs2.existsSync(filePath)) {
1105
- return;
1429
+ if (httpServer) {
1430
+ httpServer.close();
1106
1431
  }
1107
- const raw = fs2.readFileSync(filePath, "utf-8");
1108
- const data = JSON.parse(raw);
1109
- data.consumed = true;
1110
- fs2.writeFileSync(filePath, JSON.stringify(data, null, 2) + "\n");
1111
- } catch {
1112
1432
  }
1113
1433
  }
1114
1434
 
@@ -1185,170 +1505,6 @@ async function isServerAlive() {
1185
1505
  // packages/core/src/watch.ts
1186
1506
  import getPort2 from "get-port";
1187
1507
  import open2 from "open";
1188
-
1189
- // packages/core/src/watch-bridge.ts
1190
- import http2 from "http";
1191
- import { WebSocketServer as WebSocketServer2, WebSocket as WebSocket2 } from "ws";
1192
- function createWatchBridge(port, callbacks) {
1193
- let client = null;
1194
- let initPayload = null;
1195
- let pendingInit = null;
1196
- let closeTimer = null;
1197
- let submitCallback = null;
1198
- const httpServer = http2.createServer((req, res) => {
1199
- res.setHeader("Access-Control-Allow-Origin", "*");
1200
- res.setHeader("Access-Control-Allow-Methods", "GET, POST, OPTIONS");
1201
- res.setHeader("Access-Control-Allow-Headers", "Content-Type");
1202
- if (req.method === "OPTIONS") {
1203
- res.writeHead(204);
1204
- res.end();
1205
- return;
1206
- }
1207
- if (req.method === "GET" && req.url === "/api/status") {
1208
- res.writeHead(200, { "Content-Type": "application/json" });
1209
- res.end(JSON.stringify({ running: true, pid: process.pid }));
1210
- return;
1211
- }
1212
- if (req.method === "POST" && req.url === "/api/context") {
1213
- let body = "";
1214
- req.on("data", (chunk) => {
1215
- body += chunk.toString();
1216
- });
1217
- req.on("end", () => {
1218
- try {
1219
- const payload = JSON.parse(body);
1220
- callbacks.onContextUpdate(payload);
1221
- sendToClient({ type: "context:update", payload });
1222
- res.writeHead(200, { "Content-Type": "application/json" });
1223
- res.end(JSON.stringify({ ok: true }));
1224
- } catch {
1225
- res.writeHead(400, { "Content-Type": "application/json" });
1226
- res.end(JSON.stringify({ error: "Invalid JSON" }));
1227
- }
1228
- });
1229
- return;
1230
- }
1231
- if (req.method === "POST" && req.url === "/api/refresh") {
1232
- callbacks.onRefreshRequest();
1233
- res.writeHead(200, { "Content-Type": "application/json" });
1234
- res.end(JSON.stringify({ ok: true }));
1235
- return;
1236
- }
1237
- res.writeHead(404);
1238
- res.end("Not found");
1239
- });
1240
- const wss2 = new WebSocketServer2({ server: httpServer });
1241
- function sendToClient(msg) {
1242
- if (client && client.readyState === WebSocket2.OPEN) {
1243
- client.send(JSON.stringify(msg));
1244
- }
1245
- }
1246
- wss2.on("connection", (ws) => {
1247
- if (closeTimer) {
1248
- clearTimeout(closeTimer);
1249
- closeTimer = null;
1250
- }
1251
- client = ws;
1252
- const payload = pendingInit ?? initPayload;
1253
- if (payload) {
1254
- sendToClient({ type: "review:init", payload });
1255
- pendingInit = null;
1256
- }
1257
- ws.on("message", (data) => {
1258
- try {
1259
- const msg = JSON.parse(data.toString());
1260
- if (msg.type === "review:submit" && submitCallback) {
1261
- submitCallback(msg.payload);
1262
- }
1263
- } catch {
1264
- }
1265
- });
1266
- ws.on("close", () => {
1267
- client = null;
1268
- closeTimer = setTimeout(() => {
1269
- closeTimer = null;
1270
- }, 2e3);
1271
- });
1272
- });
1273
- return new Promise((resolve, reject) => {
1274
- httpServer.on("error", reject);
1275
- httpServer.listen(port, () => {
1276
- resolve({
1277
- port,
1278
- sendInit(payload) {
1279
- initPayload = payload;
1280
- if (client && client.readyState === WebSocket2.OPEN) {
1281
- sendToClient({ type: "review:init", payload });
1282
- } else {
1283
- pendingInit = payload;
1284
- }
1285
- },
1286
- storeInitPayload(payload) {
1287
- initPayload = payload;
1288
- },
1289
- sendDiffUpdate(payload) {
1290
- sendToClient({ type: "diff:update", payload });
1291
- },
1292
- sendContextUpdate(payload) {
1293
- sendToClient({ type: "context:update", payload });
1294
- },
1295
- onSubmit(callback) {
1296
- submitCallback = callback;
1297
- },
1298
- triggerRefresh() {
1299
- callbacks.onRefreshRequest();
1300
- },
1301
- async close() {
1302
- if (closeTimer) {
1303
- clearTimeout(closeTimer);
1304
- }
1305
- for (const ws of wss2.clients) {
1306
- ws.close();
1307
- }
1308
- wss2.close();
1309
- await new Promise((resolve2) => {
1310
- httpServer.close(() => resolve2());
1311
- });
1312
- }
1313
- });
1314
- });
1315
- });
1316
- }
1317
-
1318
- // packages/core/src/diff-utils.ts
1319
- import { createHash } from "crypto";
1320
- function hashDiff(rawDiff) {
1321
- return createHash("sha256").update(rawDiff).digest("hex");
1322
- }
1323
- function fileKey(file) {
1324
- return file.stage ? `${file.stage}:${file.path}` : file.path;
1325
- }
1326
- function detectChangedFiles(oldDiffSet, newDiffSet) {
1327
- if (!oldDiffSet) {
1328
- return newDiffSet.files.map(fileKey);
1329
- }
1330
- const oldFiles = new Map(
1331
- oldDiffSet.files.map((f) => [fileKey(f), f])
1332
- );
1333
- const changed = [];
1334
- for (const newFile of newDiffSet.files) {
1335
- const key = fileKey(newFile);
1336
- const oldFile = oldFiles.get(key);
1337
- if (!oldFile) {
1338
- changed.push(key);
1339
- } else if (oldFile.additions !== newFile.additions || oldFile.deletions !== newFile.deletions) {
1340
- changed.push(key);
1341
- }
1342
- }
1343
- for (const oldFile of oldDiffSet.files) {
1344
- if (!newDiffSet.files.some((f) => fileKey(f) === fileKey(oldFile))) {
1345
- changed.push(fileKey(oldFile));
1346
- }
1347
- }
1348
- return changed;
1349
- }
1350
-
1351
- // packages/core/src/watch.ts
1352
1508
  async function startWatch(options) {
1353
1509
  const {
1354
1510
  diffRef,
@@ -1363,8 +1519,6 @@ async function startWatch(options) {
1363
1519
  const { diffSet: initialDiffSet, rawDiff: initialRawDiff } = getDiff(diffRef, { cwd });
1364
1520
  const currentBranch = getCurrentBranch({ cwd });
1365
1521
  const initialBriefing = analyze(initialDiffSet);
1366
- let lastDiffHash = hashDiff(initialRawDiff);
1367
- let lastDiffSet = initialDiffSet;
1368
1522
  const metadata = {
1369
1523
  title,
1370
1524
  description,
@@ -1375,15 +1529,63 @@ async function startWatch(options) {
1375
1529
  getPort2(),
1376
1530
  getPort2()
1377
1531
  ]);
1378
- let refreshRequested = false;
1532
+ const reviewId = "watch-session";
1533
+ function handleDiffRefChange(newRef) {
1534
+ const { diffSet: newDiffSet, rawDiff: newRawDiff } = getDiff(newRef, { cwd });
1535
+ const newBriefing = analyze(newDiffSet);
1536
+ bridge.sendDiffUpdate({
1537
+ diffSet: newDiffSet,
1538
+ rawDiff: newRawDiff,
1539
+ briefing: newBriefing,
1540
+ changedFiles: newDiffSet.files.map((f) => f.path),
1541
+ timestamp: Date.now()
1542
+ });
1543
+ bridge.storeInitPayload({
1544
+ reviewId,
1545
+ diffSet: newDiffSet,
1546
+ rawDiff: newRawDiff,
1547
+ briefing: newBriefing,
1548
+ metadata,
1549
+ watchMode: true
1550
+ });
1551
+ poller.setDiffRef(newRef);
1552
+ }
1379
1553
  const bridge = await createWatchBridge(bridgePort, {
1380
1554
  onRefreshRequest: () => {
1381
- refreshRequested = true;
1555
+ poller.refresh();
1382
1556
  },
1383
1557
  onContextUpdate: (payload) => {
1384
1558
  if (payload.reasoning !== void 0) metadata.reasoning = payload.reasoning;
1385
1559
  if (payload.title !== void 0) metadata.title = payload.title;
1386
1560
  if (payload.description !== void 0) metadata.description = payload.description;
1561
+ },
1562
+ onDiffRefChange: (newRef) => {
1563
+ try {
1564
+ handleDiffRefChange(newRef);
1565
+ } catch (err) {
1566
+ bridge.sendDiffError({
1567
+ error: err instanceof Error ? err.message : String(err)
1568
+ });
1569
+ }
1570
+ },
1571
+ onRefsRequest: async () => {
1572
+ try {
1573
+ const resolvedCwd = cwd ?? process.cwd();
1574
+ const branches = listBranches({ cwd: resolvedCwd });
1575
+ const commits = listCommits({ cwd: resolvedCwd });
1576
+ const branch = getCurrentBranch({ cwd: resolvedCwd });
1577
+ return { branches, commits, currentBranch: branch };
1578
+ } catch {
1579
+ return null;
1580
+ }
1581
+ },
1582
+ onCompareRequest: async (ref) => {
1583
+ try {
1584
+ handleDiffRefChange(ref);
1585
+ return true;
1586
+ } catch {
1587
+ return false;
1588
+ }
1387
1589
  }
1388
1590
  });
1389
1591
  let httpServer = null;
@@ -1403,8 +1605,7 @@ async function startWatch(options) {
1403
1605
  diffRef,
1404
1606
  startedAt: Date.now()
1405
1607
  });
1406
- const reviewId = "watch-session";
1407
- const url = `http://localhost:${uiPort}?wsPort=${bridgePort}&reviewId=${reviewId}`;
1608
+ const url = `http://localhost:${uiPort}?wsPort=${bridgePort}&httpPort=${bridgePort}&reviewId=${reviewId}`;
1408
1609
  if (!silent) {
1409
1610
  console.log(`
1410
1611
  DiffPrism Watch: ${title ?? `watching ${diffRef}`}`);
@@ -1434,46 +1635,30 @@ Review submitted: ${result.decision}`);
1434
1635
  }
1435
1636
  writeReviewResult(cwd, result);
1436
1637
  });
1437
- let pollRunning = true;
1438
- const pollLoop = setInterval(() => {
1439
- if (!pollRunning) return;
1440
- try {
1441
- const { diffSet: newDiffSet, rawDiff: newRawDiff } = getDiff(diffRef, { cwd });
1442
- const newHash = hashDiff(newRawDiff);
1443
- if (newHash !== lastDiffHash || refreshRequested) {
1444
- refreshRequested = false;
1445
- const newBriefing = analyze(newDiffSet);
1446
- const changedFiles = detectChangedFiles(lastDiffSet, newDiffSet);
1447
- lastDiffHash = newHash;
1448
- lastDiffSet = newDiffSet;
1449
- bridge.storeInitPayload({
1450
- reviewId,
1451
- diffSet: newDiffSet,
1452
- rawDiff: newRawDiff,
1453
- briefing: newBriefing,
1454
- metadata,
1455
- watchMode: true
1456
- });
1457
- const updatePayload = {
1458
- diffSet: newDiffSet,
1459
- rawDiff: newRawDiff,
1460
- briefing: newBriefing,
1461
- changedFiles,
1462
- timestamp: Date.now()
1463
- };
1464
- bridge.sendDiffUpdate(updatePayload);
1465
- if (!silent && changedFiles.length > 0) {
1466
- console.log(
1467
- `[${(/* @__PURE__ */ new Date()).toLocaleTimeString()}] Diff updated: ${changedFiles.length} file(s) changed`
1468
- );
1469
- }
1638
+ const poller = createDiffPoller({
1639
+ diffRef,
1640
+ cwd: cwd ?? process.cwd(),
1641
+ pollInterval,
1642
+ onDiffChanged: (updatePayload) => {
1643
+ bridge.storeInitPayload({
1644
+ reviewId,
1645
+ diffSet: updatePayload.diffSet,
1646
+ rawDiff: updatePayload.rawDiff,
1647
+ briefing: updatePayload.briefing,
1648
+ metadata,
1649
+ watchMode: true
1650
+ });
1651
+ bridge.sendDiffUpdate(updatePayload);
1652
+ if (!silent && updatePayload.changedFiles.length > 0) {
1653
+ console.log(
1654
+ `[${(/* @__PURE__ */ new Date()).toLocaleTimeString()}] Diff updated: ${updatePayload.changedFiles.length} file(s) changed`
1655
+ );
1470
1656
  }
1471
- } catch {
1472
1657
  }
1473
- }, pollInterval);
1658
+ });
1659
+ poller.start();
1474
1660
  async function stop() {
1475
- pollRunning = false;
1476
- clearInterval(pollLoop);
1661
+ poller.stop();
1477
1662
  await bridge.close();
1478
1663
  if (viteServer) {
1479
1664
  await viteServer.close();
@@ -1497,7 +1682,7 @@ import http3 from "http";
1497
1682
  import { randomUUID } from "crypto";
1498
1683
  import getPort3 from "get-port";
1499
1684
  import open3 from "open";
1500
- import { WebSocketServer as WebSocketServer3, WebSocket as WebSocket3 } from "ws";
1685
+ import { WebSocketServer as WebSocketServer2, WebSocket as WebSocket2 } from "ws";
1501
1686
  var SUBMITTED_TTL_MS = 5 * 60 * 1e3;
1502
1687
  var ABANDONED_TTL_MS = 60 * 60 * 1e3;
1503
1688
  var CLEANUP_INTERVAL_MS = 60 * 1e3;
@@ -1563,7 +1748,7 @@ function broadcastToAll(msg) {
1563
1748
  if (!wss) return;
1564
1749
  const data = JSON.stringify(msg);
1565
1750
  for (const client of wss.clients) {
1566
- if (client.readyState === WebSocket3.OPEN) {
1751
+ if (client.readyState === WebSocket2.OPEN) {
1567
1752
  client.send(data);
1568
1753
  }
1569
1754
  }
@@ -1572,7 +1757,7 @@ function sendToSessionClients(sessionId, msg) {
1572
1757
  if (!wss) return;
1573
1758
  const data = JSON.stringify(msg);
1574
1759
  for (const [client, sid] of clientSessions.entries()) {
1575
- if (sid === sessionId && client.readyState === WebSocket3.OPEN) {
1760
+ if (sid === sessionId && client.readyState === WebSocket2.OPEN) {
1576
1761
  client.send(data);
1577
1762
  }
1578
1763
  }
@@ -1596,7 +1781,7 @@ function broadcastSessionRemoved(sessionId) {
1596
1781
  }
1597
1782
  function hasViewersForSession(sessionId) {
1598
1783
  for (const [client, sid] of clientSessions.entries()) {
1599
- if (sid === sessionId && client.readyState === WebSocket3.OPEN) {
1784
+ if (sid === sessionId && client.readyState === WebSocket2.OPEN) {
1600
1785
  return true;
1601
1786
  }
1602
1787
  }
@@ -1606,54 +1791,40 @@ function startSessionWatcher(sessionId) {
1606
1791
  if (sessionWatchers.has(sessionId)) return;
1607
1792
  const session = sessions2.get(sessionId);
1608
1793
  if (!session?.diffRef) return;
1609
- const interval = setInterval(() => {
1610
- const s = sessions2.get(sessionId);
1611
- if (!s?.diffRef) {
1612
- stopSessionWatcher(sessionId);
1613
- return;
1614
- }
1615
- try {
1616
- const { diffSet: newDiffSet, rawDiff: newRawDiff } = getDiff(s.diffRef, {
1617
- cwd: s.projectPath
1618
- });
1619
- const newHash = hashDiff(newRawDiff);
1620
- if (newHash !== s.lastDiffHash) {
1621
- const newBriefing = analyze(newDiffSet);
1622
- const changedFiles = detectChangedFiles(s.lastDiffSet ?? null, newDiffSet);
1623
- s.payload = {
1624
- ...s.payload,
1625
- diffSet: newDiffSet,
1626
- rawDiff: newRawDiff,
1627
- briefing: newBriefing
1628
- };
1629
- s.lastDiffHash = newHash;
1630
- s.lastDiffSet = newDiffSet;
1631
- if (hasViewersForSession(sessionId)) {
1632
- sendToSessionClients(sessionId, {
1633
- type: "diff:update",
1634
- payload: {
1635
- diffSet: newDiffSet,
1636
- rawDiff: newRawDiff,
1637
- briefing: newBriefing,
1638
- changedFiles,
1639
- timestamp: Date.now()
1640
- }
1641
- });
1642
- s.hasNewChanges = false;
1643
- } else {
1644
- s.hasNewChanges = true;
1645
- broadcastSessionList();
1646
- }
1794
+ const poller = createDiffPoller({
1795
+ diffRef: session.diffRef,
1796
+ cwd: session.projectPath,
1797
+ pollInterval: serverPollInterval,
1798
+ onDiffChanged: (updatePayload) => {
1799
+ const s = sessions2.get(sessionId);
1800
+ if (!s) return;
1801
+ s.payload = {
1802
+ ...s.payload,
1803
+ diffSet: updatePayload.diffSet,
1804
+ rawDiff: updatePayload.rawDiff,
1805
+ briefing: updatePayload.briefing
1806
+ };
1807
+ s.lastDiffHash = hashDiff(updatePayload.rawDiff);
1808
+ s.lastDiffSet = updatePayload.diffSet;
1809
+ if (hasViewersForSession(sessionId)) {
1810
+ sendToSessionClients(sessionId, {
1811
+ type: "diff:update",
1812
+ payload: updatePayload
1813
+ });
1814
+ s.hasNewChanges = false;
1815
+ } else {
1816
+ s.hasNewChanges = true;
1817
+ broadcastSessionList();
1647
1818
  }
1648
- } catch {
1649
1819
  }
1650
- }, serverPollInterval);
1651
- sessionWatchers.set(sessionId, interval);
1820
+ });
1821
+ poller.start();
1822
+ sessionWatchers.set(sessionId, poller);
1652
1823
  }
1653
1824
  function stopSessionWatcher(sessionId) {
1654
- const interval = sessionWatchers.get(sessionId);
1655
- if (interval) {
1656
- clearInterval(interval);
1825
+ const poller = sessionWatchers.get(sessionId);
1826
+ if (poller) {
1827
+ poller.stop();
1657
1828
  sessionWatchers.delete(sessionId);
1658
1829
  }
1659
1830
  }
@@ -1665,15 +1836,15 @@ function startAllWatchers() {
1665
1836
  }
1666
1837
  }
1667
1838
  function stopAllWatchers() {
1668
- for (const [id, interval] of sessionWatchers.entries()) {
1669
- clearInterval(interval);
1839
+ for (const [, poller] of sessionWatchers.entries()) {
1840
+ poller.stop();
1670
1841
  }
1671
1842
  sessionWatchers.clear();
1672
1843
  }
1673
1844
  function hasConnectedClients() {
1674
1845
  if (!wss) return false;
1675
1846
  for (const client of wss.clients) {
1676
- if (client.readyState === WebSocket3.OPEN) return true;
1847
+ if (client.readyState === WebSocket2.OPEN) return true;
1677
1848
  }
1678
1849
  return false;
1679
1850
  }
@@ -1968,7 +2139,7 @@ async function startGlobalServer(options = {}) {
1968
2139
  res.end("Not found");
1969
2140
  }
1970
2141
  });
1971
- wss = new WebSocketServer3({ port: wsPort });
2142
+ wss = new WebSocketServer2({ port: wsPort });
1972
2143
  wss.on("connection", (ws, req) => {
1973
2144
  startAllWatchers();
1974
2145
  const url = new URL(req.url ?? "/", `http://localhost:${wsPort}`);
@@ -2046,6 +2217,49 @@ async function startGlobalServer(options = {}) {
2046
2217
  closedSession.status = "submitted";
2047
2218
  }
2048
2219
  broadcastSessionRemoved(closedId);
2220
+ } else if (msg.type === "diff:change_ref") {
2221
+ const sid = clientSessions.get(ws);
2222
+ if (sid) {
2223
+ const session = sessions2.get(sid);
2224
+ if (session) {
2225
+ const newRef = msg.payload.diffRef;
2226
+ try {
2227
+ const { diffSet: newDiffSet, rawDiff: newRawDiff } = getDiff(newRef, {
2228
+ cwd: session.projectPath
2229
+ });
2230
+ const newBriefing = analyze(newDiffSet);
2231
+ session.payload = {
2232
+ ...session.payload,
2233
+ diffSet: newDiffSet,
2234
+ rawDiff: newRawDiff,
2235
+ briefing: newBriefing
2236
+ };
2237
+ session.diffRef = newRef;
2238
+ session.lastDiffHash = hashDiff(newRawDiff);
2239
+ session.lastDiffSet = newDiffSet;
2240
+ stopSessionWatcher(sid);
2241
+ startSessionWatcher(sid);
2242
+ sendToSessionClients(sid, {
2243
+ type: "diff:update",
2244
+ payload: {
2245
+ diffSet: newDiffSet,
2246
+ rawDiff: newRawDiff,
2247
+ briefing: newBriefing,
2248
+ changedFiles: newDiffSet.files.map((f) => f.path),
2249
+ timestamp: Date.now()
2250
+ }
2251
+ });
2252
+ } catch (err) {
2253
+ const errorMsg = {
2254
+ type: "diff:error",
2255
+ payload: {
2256
+ error: err instanceof Error ? err.message : String(err)
2257
+ }
2258
+ };
2259
+ ws.send(JSON.stringify(errorMsg));
2260
+ }
2261
+ }
2262
+ }
2049
2263
  }
2050
2264
  } catch {
2051
2265
  }
@@ -2130,10 +2344,10 @@ export {
2130
2344
  getCurrentBranch,
2131
2345
  getDiff,
2132
2346
  analyze,
2133
- startReview,
2134
2347
  readWatchFile,
2135
2348
  readReviewResult,
2136
2349
  consumeReviewResult,
2350
+ startReview,
2137
2351
  startWatch,
2138
2352
  readServerFile,
2139
2353
  isServerAlive,