viveworker 0.1.7 → 0.1.9

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.
@@ -283,7 +283,7 @@ function normalizeTimelineOutcome(value) {
283
283
 
284
284
  function normalizeTimelineFileEventType(value) {
285
285
  const normalized = cleanText(value || "").toLowerCase();
286
- return ["read", "write", "create"].includes(normalized) ? normalized : "";
286
+ return ["read", "write", "create", "delete", "rename"].includes(normalized) ? normalized : "";
287
287
  }
288
288
 
289
289
  function fileEventTitle(locale, fileEventType) {
@@ -294,6 +294,10 @@ function fileEventTitle(locale, fileEventType) {
294
294
  return t(locale, "fileEvent.write");
295
295
  case "create":
296
296
  return t(locale, "fileEvent.create");
297
+ case "delete":
298
+ return t(locale, "fileEvent.delete");
299
+ case "rename":
300
+ return t(locale, "fileEvent.rename");
297
301
  default:
298
302
  return t(locale, "common.fileEvent");
299
303
  }
@@ -307,6 +311,10 @@ function fileEventDetailCopy(locale, fileEventType) {
307
311
  return t(locale, "detail.fileEvent.write");
308
312
  case "create":
309
313
  return t(locale, "detail.fileEvent.create");
314
+ case "delete":
315
+ return t(locale, "detail.fileEvent.delete");
316
+ case "rename":
317
+ return t(locale, "detail.fileEvent.rename");
310
318
  default:
311
319
  return t(locale, "detail.detailUnavailable");
312
320
  }
@@ -618,16 +626,82 @@ function extractReadFileRefsFromCommand(commandText) {
618
626
  return [];
619
627
  }
620
628
 
621
- function extractUpdatedFileRefsByType(outputText) {
629
+ function extractUpdatedFileRefsByType(outputText, patchText = "") {
630
+ const parsedSections = parseApplyPatchSections(patchText);
631
+ if (parsedSections.length > 0) {
632
+ const createRefs = [];
633
+ const writeRefs = [];
634
+ const deleteRefs = [];
635
+ const renameRefs = [];
636
+ for (const section of parsedSections) {
637
+ const newFileRef = cleanTimelineFileRef(section?.newFileRef || section?.fileRef || "");
638
+ const oldFileRef = cleanTimelineFileRef(section?.oldFileRef || "");
639
+ switch (section?.kind) {
640
+ case "create":
641
+ if (newFileRef) {
642
+ createRefs.push(newFileRef);
643
+ }
644
+ break;
645
+ case "write":
646
+ if (newFileRef) {
647
+ writeRefs.push(newFileRef);
648
+ }
649
+ break;
650
+ case "delete":
651
+ if (oldFileRef || newFileRef) {
652
+ deleteRefs.push(oldFileRef || newFileRef);
653
+ }
654
+ break;
655
+ case "rename":
656
+ if (oldFileRef && newFileRef) {
657
+ renameRefs.push({
658
+ oldFileRef,
659
+ newFileRef,
660
+ });
661
+ }
662
+ break;
663
+ default:
664
+ break;
665
+ }
666
+ }
667
+ return {
668
+ create: normalizeTimelineFileRefs(createRefs),
669
+ write: normalizeTimelineFileRefs(writeRefs),
670
+ delete: normalizeTimelineFileRefs(deleteRefs),
671
+ rename: renameRefs.filter(
672
+ (entry, index, array) =>
673
+ array.findIndex(
674
+ (candidate) =>
675
+ timelineFileRefsMatch(candidate.oldFileRef, entry.oldFileRef) &&
676
+ timelineFileRefsMatch(candidate.newFileRef, entry.newFileRef)
677
+ ) === index
678
+ ),
679
+ };
680
+ }
681
+
622
682
  const parsed = safeJsonParse(outputText);
623
683
  const sourceText = typeof parsed?.output === "string" ? parsed.output : String(outputText || "");
624
684
  if (!/^Success\. Updated the following files:/mu.test(sourceText)) {
625
- return { create: [], write: [] };
685
+ return { create: [], write: [], delete: [], rename: [] };
626
686
  }
627
687
 
628
688
  const createRefs = [];
629
689
  const writeRefs = [];
690
+ const deleteRefs = [];
691
+ const renameRefs = [];
630
692
  for (const line of sourceText.split("\n")) {
693
+ const renameMatch =
694
+ line.match(/^R\d*\s+(.+?)\s+->\s+(.+)$/u) ||
695
+ line.match(/^R\d*\s+(.+?)\t(.+)$/u);
696
+ if (renameMatch) {
697
+ const oldFileRef = cleanTimelineFileRef(renameMatch[1]);
698
+ const newFileRef = cleanTimelineFileRef(renameMatch[2]);
699
+ if (oldFileRef && newFileRef) {
700
+ renameRefs.push({ oldFileRef, newFileRef });
701
+ }
702
+ continue;
703
+ }
704
+
631
705
  const match = line.match(/^([AMD])\s+(.+)$/u);
632
706
  if (!match) {
633
707
  continue;
@@ -640,12 +714,23 @@ function extractUpdatedFileRefsByType(outputText) {
640
714
  createRefs.push(fileRef);
641
715
  } else if (match[1] === "M") {
642
716
  writeRefs.push(fileRef);
717
+ } else if (match[1] === "D") {
718
+ deleteRefs.push(fileRef);
643
719
  }
644
720
  }
645
721
 
646
722
  return {
647
723
  create: normalizeTimelineFileRefs(createRefs),
648
724
  write: normalizeTimelineFileRefs(writeRefs),
725
+ delete: normalizeTimelineFileRefs(deleteRefs),
726
+ rename: renameRefs.filter(
727
+ (entry, index, array) =>
728
+ array.findIndex(
729
+ (candidate) =>
730
+ timelineFileRefsMatch(candidate.oldFileRef, entry.oldFileRef) &&
731
+ timelineFileRefsMatch(candidate.newFileRef, entry.newFileRef)
732
+ ) === index
733
+ ),
649
734
  };
650
735
  }
651
736
 
@@ -804,6 +889,8 @@ function parseApplyPatchSections(patchText) {
804
889
  sections.push({
805
890
  kind: current.kind,
806
891
  fileRef: current.fileRef,
892
+ oldFileRef: current.oldFileRef || "",
893
+ newFileRef: current.newFileRef || current.fileRef,
807
894
  bodyLines: [...current.bodyLines],
808
895
  });
809
896
  current = null;
@@ -816,6 +903,8 @@ function parseApplyPatchSections(patchText) {
816
903
  current = {
817
904
  kind: "create",
818
905
  fileRef: cleanTimelineFileRef(addMatch[1]),
906
+ oldFileRef: "",
907
+ newFileRef: cleanTimelineFileRef(addMatch[1]),
819
908
  bodyLines: [],
820
909
  };
821
910
  continue;
@@ -827,6 +916,8 @@ function parseApplyPatchSections(patchText) {
827
916
  current = {
828
917
  kind: "write",
829
918
  fileRef: cleanTimelineFileRef(updateMatch[1]),
919
+ oldFileRef: cleanTimelineFileRef(updateMatch[1]),
920
+ newFileRef: cleanTimelineFileRef(updateMatch[1]),
830
921
  bodyLines: [],
831
922
  };
832
923
  continue;
@@ -838,6 +929,8 @@ function parseApplyPatchSections(patchText) {
838
929
  current = {
839
930
  kind: "delete",
840
931
  fileRef: cleanTimelineFileRef(deleteMatch[1]),
932
+ oldFileRef: cleanTimelineFileRef(deleteMatch[1]),
933
+ newFileRef: "",
841
934
  bodyLines: [],
842
935
  };
843
936
  continue;
@@ -851,6 +944,9 @@ function parseApplyPatchSections(patchText) {
851
944
  if (moveMatch) {
852
945
  const movedFileRef = cleanTimelineFileRef(moveMatch[1]);
853
946
  if (movedFileRef) {
947
+ current.kind = current.kind === "write" ? "rename" : current.kind;
948
+ current.oldFileRef = current.oldFileRef || current.fileRef;
949
+ current.newFileRef = movedFileRef;
854
950
  current.fileRef = movedFileRef;
855
951
  }
856
952
  continue;
@@ -873,27 +969,29 @@ function parseApplyPatchSections(patchText) {
873
969
  }
874
970
 
875
971
  function buildUnifiedDiffFromApplyPatchSection(section) {
876
- if (!section?.fileRef) {
972
+ if (!section?.fileRef && !section?.oldFileRef) {
877
973
  return "";
878
974
  }
879
975
 
880
976
  const bodyLines = Array.isArray(section.bodyLines) ? section.bodyLines : [];
881
- const fileRef = section.fileRef;
882
- const diffLines = [`diff --git ${diffPathForSide(fileRef, "a")} ${diffPathForSide(fileRef, "b")}`];
977
+ const fileRef = cleanTimelineFileRef(section.fileRef || section.newFileRef || "");
978
+ const oldFileRef = cleanTimelineFileRef(section.oldFileRef || fileRef);
979
+ const newFileRef = cleanTimelineFileRef(section.newFileRef || fileRef);
980
+ const diffLines = [`diff --git ${diffPathForSide(oldFileRef || fileRef, "a")} ${diffPathForSide(newFileRef || fileRef, "b")}`];
883
981
 
884
982
  if (section.kind === "create") {
885
983
  const addedCount = bodyLines.filter((line) => line.startsWith("+")).length;
886
984
  diffLines.push("new file mode 100644");
887
985
  diffLines.push("--- /dev/null");
888
- diffLines.push(`+++ ${diffPathForSide(fileRef, "b")}`);
986
+ diffLines.push(`+++ ${diffPathForSide(newFileRef || fileRef, "b")}`);
889
987
  diffLines.push(`@@ -0,0 +1,${Math.max(addedCount, 1)} @@`);
890
988
  diffLines.push(...bodyLines);
891
989
  return normalizeTimelineDiffText(diffLines.join("\n"));
892
990
  }
893
991
 
894
992
  if (section.kind === "write") {
895
- diffLines.push(`--- ${diffPathForSide(fileRef, "a")}`);
896
- diffLines.push(`+++ ${diffPathForSide(fileRef, "b")}`);
993
+ diffLines.push(`--- ${diffPathForSide(oldFileRef || fileRef, "a")}`);
994
+ diffLines.push(`+++ ${diffPathForSide(newFileRef || fileRef, "b")}`);
897
995
  diffLines.push(...bodyLines);
898
996
  return normalizeTimelineDiffText(diffLines.join("\n"));
899
997
  }
@@ -901,26 +999,49 @@ function buildUnifiedDiffFromApplyPatchSection(section) {
901
999
  if (section.kind === "delete") {
902
1000
  const removedCount = bodyLines.filter((line) => line.startsWith("-")).length;
903
1001
  diffLines.push("deleted file mode 100644");
904
- diffLines.push(`--- ${diffPathForSide(fileRef, "a")}`);
1002
+ diffLines.push(`--- ${diffPathForSide(oldFileRef || fileRef, "a")}`);
905
1003
  diffLines.push("+++ /dev/null");
906
1004
  diffLines.push(`@@ -1,${Math.max(removedCount, 1)} +0,0 @@`);
907
1005
  diffLines.push(...bodyLines);
908
1006
  return normalizeTimelineDiffText(diffLines.join("\n"));
909
1007
  }
910
1008
 
1009
+ if (section.kind === "rename") {
1010
+ diffLines.push(`rename from ${oldFileRef || fileRef}`);
1011
+ diffLines.push(`rename to ${newFileRef || fileRef}`);
1012
+ if (bodyLines.length > 0) {
1013
+ diffLines.push(`--- ${diffPathForSide(oldFileRef || fileRef, "a")}`);
1014
+ diffLines.push(`+++ ${diffPathForSide(newFileRef || fileRef, "b")}`);
1015
+ diffLines.push(...bodyLines);
1016
+ }
1017
+ return normalizeTimelineDiffText(diffLines.join("\n"));
1018
+ }
1019
+
911
1020
  return "";
912
1021
  }
913
1022
 
914
- function buildApplyPatchDiffForFileRefs(patchText, fileRefs, fileEventType) {
1023
+ function buildApplyPatchDiffForFileRefs(patchText, fileRefs, fileEventType, previousFileRefs = []) {
915
1024
  const normalizedRefs = normalizeTimelineFileRefs(fileRefs);
1025
+ const normalizedPreviousRefs = normalizeTimelineFileRefs(previousFileRefs);
916
1026
  if (!normalizedRefs.length) {
917
- return "";
1027
+ if (normalizeTimelineFileEventType(fileEventType) !== "rename" || !normalizedPreviousRefs.length) {
1028
+ return "";
1029
+ }
918
1030
  }
919
1031
 
920
1032
  const sections = parseApplyPatchSections(patchText).filter((section) => {
921
1033
  if (!section?.fileRef || section.kind !== fileEventType) {
922
1034
  return false;
923
1035
  }
1036
+ if (fileEventType === "rename") {
1037
+ return (
1038
+ normalizedRefs.some((fileRef) => timelineFileRefsMatch(fileRef, section.newFileRef || section.fileRef)) ||
1039
+ normalizedPreviousRefs.some((fileRef) => timelineFileRefsMatch(fileRef, section.oldFileRef || ""))
1040
+ );
1041
+ }
1042
+ if (fileEventType === "delete") {
1043
+ return normalizedRefs.some((fileRef) => timelineFileRefsMatch(fileRef, section.oldFileRef || section.fileRef));
1044
+ }
924
1045
  return normalizedRefs.some((fileRef) => timelineFileRefsMatch(fileRef, section.fileRef));
925
1046
  });
926
1047
 
@@ -958,7 +1079,7 @@ async function captureGitDiffText({ cwd, fileRefs }) {
958
1079
  }
959
1080
 
960
1081
  return new Promise((resolve) => {
961
- const child = spawn("git", ["diff", "--no-ext-diff", "--no-color", "--", ...normalizedFileRefs], {
1082
+ const child = spawn("git", ["diff", "--no-ext-diff", "--no-color", "-M", "--find-renames", "--", ...normalizedFileRefs], {
962
1083
  cwd: normalizedCwd,
963
1084
  stdio: ["ignore", "pipe", "pipe"],
964
1085
  });
@@ -998,9 +1119,16 @@ function gitPathspecForFileRef(cwd, fileRef) {
998
1119
  return relativePath;
999
1120
  }
1000
1121
 
1001
- async function buildFileEventDiff({ fileState, callId, fileRefs, fileEventType, rolloutFilePath = "" }) {
1122
+ async function buildFileEventDiff({
1123
+ fileState,
1124
+ callId,
1125
+ fileRefs,
1126
+ fileEventType,
1127
+ previousFileRefs = [],
1128
+ rolloutFilePath = "",
1129
+ }) {
1002
1130
  const normalizedFileEventType = normalizeTimelineFileEventType(fileEventType);
1003
- if (!["write", "create"].includes(normalizedFileEventType)) {
1131
+ if (!["write", "create", "delete", "rename"].includes(normalizedFileEventType)) {
1004
1132
  return {
1005
1133
  diffText: "",
1006
1134
  diffSource: "",
@@ -1015,13 +1143,18 @@ async function buildFileEventDiff({ fileState, callId, fileRefs, fileEventType,
1015
1143
  callId,
1016
1144
  rolloutFilePath,
1017
1145
  });
1018
- let diffText = buildApplyPatchDiffForFileRefs(storedPatch?.inputText || "", fileRefs, normalizedFileEventType);
1146
+ let diffText = buildApplyPatchDiffForFileRefs(
1147
+ storedPatch?.inputText || "",
1148
+ fileRefs,
1149
+ normalizedFileEventType,
1150
+ previousFileRefs
1151
+ );
1019
1152
  let diffSource = diffText ? "apply_patch" : "";
1020
1153
 
1021
1154
  if (!diffText) {
1022
1155
  diffText = await captureGitDiffText({
1023
1156
  cwd: cleanText(storedPatch?.cwd || fileState?.cwd || ""),
1024
- fileRefs,
1157
+ fileRefs: [...normalizeTimelineFileRefs(previousFileRefs), ...normalizeTimelineFileRefs(fileRefs)],
1025
1158
  });
1026
1159
  diffSource = diffText ? "git" : "";
1027
1160
  }
@@ -1124,7 +1257,33 @@ function normalizeUnifiedDiffSectionFileRef(value) {
1124
1257
  }
1125
1258
 
1126
1259
  function extractUnifiedDiffSectionFileRef(sectionText) {
1260
+ const paths = extractUnifiedDiffSectionPaths(sectionText);
1261
+ if (paths.newFileRef) {
1262
+ return paths.newFileRef;
1263
+ }
1264
+ if (paths.oldFileRef) {
1265
+ return paths.oldFileRef;
1266
+ }
1267
+ return "";
1268
+ }
1269
+
1270
+ function extractUnifiedDiffSectionPaths(sectionText) {
1127
1271
  const lines = String(sectionText || "").replace(/\r\n/gu, "\n").split("\n");
1272
+ let oldFileRef = "";
1273
+ let newFileRef = "";
1274
+
1275
+ for (const line of lines) {
1276
+ const diffMatch = line.match(/^diff --git\s+(\S+)\s+(\S+)$/u);
1277
+ if (!diffMatch) {
1278
+ continue;
1279
+ }
1280
+ oldFileRef = normalizeUnifiedDiffSectionFileRef(diffMatch[1]);
1281
+ newFileRef = normalizeUnifiedDiffSectionFileRef(diffMatch[2]);
1282
+ if (oldFileRef || newFileRef) {
1283
+ return { oldFileRef, newFileRef };
1284
+ }
1285
+ }
1286
+
1128
1287
  const preferredPrefixes = ["+++ ", "--- "];
1129
1288
  for (const prefix of preferredPrefixes) {
1130
1289
  for (const line of lines) {
@@ -1133,11 +1292,16 @@ function extractUnifiedDiffSectionFileRef(sectionText) {
1133
1292
  }
1134
1293
  const fileRef = normalizeUnifiedDiffSectionFileRef(line.slice(prefix.length));
1135
1294
  if (fileRef) {
1136
- return fileRef;
1295
+ if (prefix === "+++ ") {
1296
+ newFileRef = fileRef;
1297
+ } else {
1298
+ oldFileRef = fileRef;
1299
+ }
1137
1300
  }
1138
1301
  }
1139
1302
  }
1140
- return "";
1303
+
1304
+ return { oldFileRef, newFileRef };
1141
1305
  }
1142
1306
 
1143
1307
  function splitUnifiedDiffTextByFile(diffText) {
@@ -1160,6 +1324,7 @@ function splitUnifiedDiffTextByFile(diffText) {
1160
1324
  return;
1161
1325
  }
1162
1326
  sections.push({
1327
+ ...extractUnifiedDiffSectionPaths(sectionText),
1163
1328
  fileRef: extractUnifiedDiffSectionFileRef(sectionText),
1164
1329
  diffText: sectionText,
1165
1330
  });
@@ -1183,6 +1348,414 @@ function splitUnifiedDiffTextByFile(diffText) {
1183
1348
  return sections.filter((section) => section.diffText);
1184
1349
  }
1185
1350
 
1351
+ function codeOwnershipKeyForFile(fileRef) {
1352
+ const normalized = cleanTimelineFileRef(fileRef);
1353
+ return normalized ? `file:${normalized}` : "";
1354
+ }
1355
+
1356
+ function codeOwnershipKeyForDelete(fileRef) {
1357
+ const normalized = cleanTimelineFileRef(fileRef);
1358
+ return normalized ? `delete:${normalized}` : "";
1359
+ }
1360
+
1361
+ function codeOwnershipKeyForRename(oldFileRef, newFileRef) {
1362
+ const normalizedOldFileRef = cleanTimelineFileRef(oldFileRef);
1363
+ const normalizedNewFileRef = cleanTimelineFileRef(newFileRef);
1364
+ return normalizedOldFileRef && normalizedNewFileRef
1365
+ ? `rename:${normalizedOldFileRef}=>${normalizedNewFileRef}`
1366
+ : "";
1367
+ }
1368
+
1369
+ function normalizeRepoRelativeFileRef({ repoRoot, fileRef, cwd = "" }) {
1370
+ const normalizedRepoRoot = resolvePath(cleanText(repoRoot || ""));
1371
+ const normalizedFileRef = cleanTimelineFileRef(fileRef);
1372
+ const normalizedCwd = resolvePath(cleanText(cwd || ""));
1373
+ if (!normalizedRepoRoot || !normalizedFileRef) {
1374
+ return "";
1375
+ }
1376
+
1377
+ if (path.isAbsolute(normalizedFileRef)) {
1378
+ const relativePath = path.relative(normalizedRepoRoot, resolvePath(normalizedFileRef));
1379
+ if (!relativePath || relativePath.startsWith("..") || path.isAbsolute(relativePath)) {
1380
+ return "";
1381
+ }
1382
+ return cleanTimelineFileRef(relativePath);
1383
+ }
1384
+
1385
+ if (normalizedCwd) {
1386
+ const absolutePath = path.resolve(normalizedCwd, normalizedFileRef);
1387
+ const relativePath = path.relative(normalizedRepoRoot, absolutePath);
1388
+ if (relativePath && !relativePath.startsWith("..") && !path.isAbsolute(relativePath)) {
1389
+ return cleanTimelineFileRef(relativePath);
1390
+ }
1391
+ }
1392
+
1393
+ return normalizedFileRef;
1394
+ }
1395
+
1396
+ async function captureGitOutput({ cwd, args }) {
1397
+ const normalizedCwd = resolvePath(cleanText(cwd || ""));
1398
+ if (!normalizedCwd || !Array.isArray(args) || args.length === 0) {
1399
+ return "";
1400
+ }
1401
+
1402
+ return new Promise((resolve) => {
1403
+ const child = spawn("git", args, {
1404
+ cwd: normalizedCwd,
1405
+ stdio: ["ignore", "pipe", "pipe"],
1406
+ });
1407
+
1408
+ let stdout = "";
1409
+ child.stdout.on("data", (chunk) => {
1410
+ stdout += chunk.toString("utf8");
1411
+ });
1412
+ child.on("error", () => {
1413
+ resolve("");
1414
+ });
1415
+ child.on("close", (code) => {
1416
+ if (code !== 0) {
1417
+ resolve("");
1418
+ return;
1419
+ }
1420
+ resolve(String(stdout || "").replace(/\r\n/gu, "\n").trim());
1421
+ });
1422
+ });
1423
+ }
1424
+
1425
+ async function captureGitRepoRoot(searchDir) {
1426
+ return cleanText(
1427
+ await captureGitOutput({
1428
+ cwd: searchDir,
1429
+ args: ["rev-parse", "--show-toplevel"],
1430
+ })
1431
+ );
1432
+ }
1433
+
1434
+ async function resolveCodeEventRepoRoot(item, repoRootCache) {
1435
+ const searchDirs = [];
1436
+ const normalizedCwd = resolvePath(cleanText(item?.cwd || ""));
1437
+ if (normalizedCwd) {
1438
+ searchDirs.push(normalizedCwd);
1439
+ }
1440
+
1441
+ for (const fileRef of [
1442
+ ...normalizeTimelineFileRefs(item?.fileRefs ?? []),
1443
+ ...normalizeTimelineFileRefs(item?.previousFileRefs ?? []),
1444
+ ]) {
1445
+ if (!path.isAbsolute(fileRef)) {
1446
+ continue;
1447
+ }
1448
+ searchDirs.push(path.dirname(resolvePath(fileRef)));
1449
+ }
1450
+
1451
+ for (const searchDir of [...new Set(searchDirs.filter(Boolean))]) {
1452
+ if (repoRootCache.has(searchDir)) {
1453
+ const cached = cleanText(repoRootCache.get(searchDir) || "");
1454
+ if (cached) {
1455
+ return cached;
1456
+ }
1457
+ continue;
1458
+ }
1459
+ const repoRoot = await captureGitRepoRoot(searchDir);
1460
+ repoRootCache.set(searchDir, repoRoot);
1461
+ if (repoRoot) {
1462
+ return repoRoot;
1463
+ }
1464
+ }
1465
+
1466
+ return "";
1467
+ }
1468
+
1469
+ function expandCodeEventOwnershipCandidates(item, repoRoot) {
1470
+ const fileEventType = normalizeTimelineFileEventType(item?.fileEventType || "");
1471
+ const createdAtMs = Number(item?.createdAtMs) || 0;
1472
+ const cwd = cleanText(item?.cwd || "");
1473
+ const threadId = cleanText(item?.threadId || "");
1474
+ const threadLabel = cleanText(item?.threadLabel || "");
1475
+ const fileRefs = normalizeTimelineFileRefs(item?.fileRefs ?? []).map((fileRef) =>
1476
+ normalizeRepoRelativeFileRef({ repoRoot, fileRef, cwd })
1477
+ );
1478
+ const previousFileRefs = normalizeTimelineFileRefs(item?.previousFileRefs ?? []).map((fileRef) =>
1479
+ normalizeRepoRelativeFileRef({ repoRoot, fileRef, cwd })
1480
+ );
1481
+
1482
+ if (fileEventType === "rename") {
1483
+ const renameCandidates = [];
1484
+ const pairCount = Math.max(fileRefs.length, previousFileRefs.length);
1485
+ for (let index = 0; index < pairCount; index += 1) {
1486
+ const newFileRef = cleanTimelineFileRef(fileRefs[index] || "");
1487
+ const oldFileRef = cleanTimelineFileRef(previousFileRefs[index] || "");
1488
+ if (!newFileRef && !oldFileRef) {
1489
+ continue;
1490
+ }
1491
+ const ownershipKeys = [
1492
+ codeOwnershipKeyForRename(oldFileRef, newFileRef),
1493
+ codeOwnershipKeyForFile(newFileRef),
1494
+ ].filter(Boolean);
1495
+ renameCandidates.push({
1496
+ threadId,
1497
+ threadLabel,
1498
+ createdAtMs,
1499
+ fileEventType,
1500
+ ownershipKeys,
1501
+ fileRef: newFileRef,
1502
+ previousFileRef: oldFileRef,
1503
+ });
1504
+ }
1505
+ return renameCandidates;
1506
+ }
1507
+
1508
+ const normalizedRefs = fileRefs.filter(Boolean);
1509
+ return normalizedRefs.map((fileRef) => ({
1510
+ threadId,
1511
+ threadLabel,
1512
+ createdAtMs,
1513
+ fileEventType,
1514
+ ownershipKeys: [
1515
+ fileEventType === "delete" ? codeOwnershipKeyForDelete(fileRef) : codeOwnershipKeyForFile(fileRef),
1516
+ ].filter(Boolean),
1517
+ fileRef,
1518
+ previousFileRef: "",
1519
+ }));
1520
+ }
1521
+
1522
+ function collectCodeCandidatePathspecs(candidates) {
1523
+ const pathspecs = new Set();
1524
+ for (const candidate of candidates) {
1525
+ const fileRef = cleanTimelineFileRef(candidate?.fileRef || "");
1526
+ const previousFileRef = cleanTimelineFileRef(candidate?.previousFileRef || "");
1527
+ if (fileRef) {
1528
+ pathspecs.add(fileRef);
1529
+ }
1530
+ if (previousFileRef) {
1531
+ pathspecs.add(previousFileRef);
1532
+ }
1533
+ }
1534
+ return [...pathspecs];
1535
+ }
1536
+
1537
+ async function captureGitDiffNameStatus({ repoRoot, pathspecs }) {
1538
+ return captureGitOutput({
1539
+ cwd: repoRoot,
1540
+ args: ["diff", "--name-status", "-M", "--find-renames", "--", ...pathspecs],
1541
+ });
1542
+ }
1543
+
1544
+ async function captureGitStatusPorcelain({ repoRoot, pathspecs }) {
1545
+ return captureGitOutput({
1546
+ cwd: repoRoot,
1547
+ args: ["status", "--porcelain=v1", "--untracked-files=all", "--", ...pathspecs],
1548
+ });
1549
+ }
1550
+
1551
+ async function captureGitCurrentDiffText({ repoRoot, pathspecs }) {
1552
+ return captureGitOutput({
1553
+ cwd: repoRoot,
1554
+ args: ["diff", "--no-ext-diff", "--no-color", "-M", "--find-renames", "--", ...pathspecs],
1555
+ });
1556
+ }
1557
+
1558
+ function parseGitNameStatusEntries(outputText) {
1559
+ const entries = [];
1560
+ for (const rawLine of String(outputText || "").split("\n")) {
1561
+ const line = rawLine.trim();
1562
+ if (!line) {
1563
+ continue;
1564
+ }
1565
+ const parts = line.split("\t");
1566
+ const status = cleanText(parts[0] || "");
1567
+ if (!status) {
1568
+ continue;
1569
+ }
1570
+ if (status.startsWith("R")) {
1571
+ const oldFileRef = cleanTimelineFileRef(parts[1] || "");
1572
+ const newFileRef = cleanTimelineFileRef(parts[2] || "");
1573
+ if (oldFileRef && newFileRef) {
1574
+ entries.push({
1575
+ changeType: "rename",
1576
+ fileRef: newFileRef,
1577
+ oldFileRef,
1578
+ newFileRef,
1579
+ });
1580
+ }
1581
+ continue;
1582
+ }
1583
+ const fileRef = cleanTimelineFileRef(parts[1] || "");
1584
+ if (!fileRef) {
1585
+ continue;
1586
+ }
1587
+ if (status === "M") {
1588
+ entries.push({
1589
+ changeType: "write",
1590
+ fileRef,
1591
+ oldFileRef: "",
1592
+ newFileRef: fileRef,
1593
+ });
1594
+ } else if (status === "D") {
1595
+ entries.push({
1596
+ changeType: "delete",
1597
+ fileRef,
1598
+ oldFileRef: fileRef,
1599
+ newFileRef: "",
1600
+ });
1601
+ }
1602
+ }
1603
+ return entries;
1604
+ }
1605
+
1606
+ function parseGitUntrackedEntries(outputText) {
1607
+ const fileRefs = [];
1608
+ for (const rawLine of String(outputText || "").split("\n")) {
1609
+ const line = String(rawLine || "");
1610
+ if (!line.startsWith("?? ")) {
1611
+ continue;
1612
+ }
1613
+ const fileRef = cleanTimelineFileRef(line.slice(3));
1614
+ if (fileRef) {
1615
+ fileRefs.push(fileRef);
1616
+ }
1617
+ }
1618
+ return normalizeTimelineFileRefs(fileRefs);
1619
+ }
1620
+
1621
+ async function buildUntrackedUnifiedDiff({ repoRoot, fileRef }) {
1622
+ const normalizedRepoRoot = resolvePath(cleanText(repoRoot || ""));
1623
+ const normalizedFileRef = cleanTimelineFileRef(fileRef);
1624
+ if (!normalizedRepoRoot || !normalizedFileRef) {
1625
+ return "";
1626
+ }
1627
+
1628
+ const absolutePath = path.join(normalizedRepoRoot, normalizedFileRef);
1629
+ let content = "";
1630
+ try {
1631
+ content = await fs.readFile(absolutePath, "utf8");
1632
+ } catch {
1633
+ return "";
1634
+ }
1635
+
1636
+ const lines = String(content || "").replace(/\r\n/gu, "\n").split("\n");
1637
+ const bodyLines = lines.length === 1 && lines[0] === "" ? [] : lines.map((line) => `+${line}`);
1638
+ const diffLines = [
1639
+ `diff --git ${diffPathForSide(normalizedFileRef, "a")} ${diffPathForSide(normalizedFileRef, "b")}`,
1640
+ "new file mode 100644",
1641
+ "--- /dev/null",
1642
+ `+++ ${diffPathForSide(normalizedFileRef, "b")}`,
1643
+ ];
1644
+ if (bodyLines.length > 0) {
1645
+ diffLines.push(`@@ -0,0 +1,${Math.max(bodyLines.length, 1)} @@`);
1646
+ diffLines.push(...bodyLines);
1647
+ }
1648
+ return normalizeTimelineDiffText(diffLines.join("\n"));
1649
+ }
1650
+
1651
+ function findMatchingCurrentDiffSection(sections, change) {
1652
+ if (!Array.isArray(sections) || sections.length === 0 || !isPlainObject(change)) {
1653
+ return null;
1654
+ }
1655
+ const changeType = normalizeTimelineFileEventType(change.changeType || "");
1656
+ if (changeType === "rename") {
1657
+ return (
1658
+ sections.find(
1659
+ (section) =>
1660
+ timelineFileRefsMatch(section.oldFileRef || "", change.oldFileRef || "") &&
1661
+ timelineFileRefsMatch(section.newFileRef || section.fileRef || "", change.newFileRef || "")
1662
+ ) ?? null
1663
+ );
1664
+ }
1665
+ if (changeType === "delete") {
1666
+ return (
1667
+ sections.find((section) =>
1668
+ timelineFileRefsMatch(section.oldFileRef || section.fileRef || "", change.fileRef || change.oldFileRef || "")
1669
+ ) ?? null
1670
+ );
1671
+ }
1672
+ return (
1673
+ sections.find((section) =>
1674
+ timelineFileRefsMatch(section.newFileRef || section.fileRef || "", change.fileRef || change.newFileRef || "")
1675
+ ) ?? null
1676
+ );
1677
+ }
1678
+
1679
+ async function buildCurrentUnstagedChangesForRepo({ repoRoot, candidates }) {
1680
+ const pathspecs = collectCodeCandidatePathspecs(candidates);
1681
+ if (!repoRoot || pathspecs.length === 0) {
1682
+ return [];
1683
+ }
1684
+
1685
+ const [nameStatusText, statusText, diffText] = await Promise.all([
1686
+ captureGitDiffNameStatus({ repoRoot, pathspecs }),
1687
+ captureGitStatusPorcelain({ repoRoot, pathspecs }),
1688
+ captureGitCurrentDiffText({ repoRoot, pathspecs }),
1689
+ ]);
1690
+
1691
+ const diffSections = splitUnifiedDiffTextByFile(diffText);
1692
+ const currentChanges = parseGitNameStatusEntries(nameStatusText).map((entry) => {
1693
+ const section = findMatchingCurrentDiffSection(diffSections, entry);
1694
+ const bestDiffText =
1695
+ normalizeTimelineDiffText(section?.diffText || "") ||
1696
+ (entry.changeType === "rename"
1697
+ ? buildUnifiedDiffFromApplyPatchSection({
1698
+ kind: "rename",
1699
+ fileRef: entry.newFileRef || entry.fileRef,
1700
+ oldFileRef: entry.oldFileRef,
1701
+ newFileRef: entry.newFileRef || entry.fileRef,
1702
+ bodyLines: [],
1703
+ })
1704
+ : "");
1705
+ const counts = diffLineCounts(bestDiffText);
1706
+ return {
1707
+ changeType: normalizeTimelineFileEventType(entry.changeType || ""),
1708
+ fileRef: cleanTimelineFileRef(entry.fileRef || entry.newFileRef || ""),
1709
+ oldFileRef: cleanTimelineFileRef(entry.oldFileRef || ""),
1710
+ newFileRef: cleanTimelineFileRef(entry.newFileRef || entry.fileRef || ""),
1711
+ diffText: bestDiffText,
1712
+ diffAvailable: Boolean(bestDiffText),
1713
+ diffSource: bestDiffText ? "git" : "",
1714
+ diffAddedLines: counts.addedLines,
1715
+ diffRemovedLines: counts.removedLines,
1716
+ };
1717
+ });
1718
+
1719
+ for (const fileRef of parseGitUntrackedEntries(statusText)) {
1720
+ const syntheticDiffText = await buildUntrackedUnifiedDiff({ repoRoot, fileRef });
1721
+ const counts = diffLineCounts(syntheticDiffText);
1722
+ currentChanges.push({
1723
+ changeType: "create",
1724
+ fileRef,
1725
+ oldFileRef: "",
1726
+ newFileRef: fileRef,
1727
+ diffText: syntheticDiffText,
1728
+ diffAvailable: Boolean(syntheticDiffText),
1729
+ diffSource: syntheticDiffText ? "git" : "",
1730
+ diffAddedLines: counts.addedLines,
1731
+ diffRemovedLines: counts.removedLines,
1732
+ });
1733
+ }
1734
+
1735
+ return currentChanges;
1736
+ }
1737
+
1738
+ function resolveCurrentChangeOwner(change, candidates) {
1739
+ const preferredKeys = [];
1740
+ const changeType = normalizeTimelineFileEventType(change?.changeType || "");
1741
+ if (changeType === "rename") {
1742
+ preferredKeys.push(codeOwnershipKeyForRename(change?.oldFileRef || "", change?.newFileRef || ""));
1743
+ preferredKeys.push(codeOwnershipKeyForFile(change?.newFileRef || change?.fileRef || ""));
1744
+ } else if (changeType === "delete") {
1745
+ preferredKeys.push(codeOwnershipKeyForDelete(change?.fileRef || change?.oldFileRef || ""));
1746
+ } else {
1747
+ preferredKeys.push(codeOwnershipKeyForFile(change?.fileRef || change?.newFileRef || ""));
1748
+ }
1749
+
1750
+ for (const key of preferredKeys.filter(Boolean)) {
1751
+ const match = candidates.find((candidate) => Array.isArray(candidate?.ownershipKeys) && candidate.ownershipKeys.includes(key));
1752
+ if (match) {
1753
+ return match;
1754
+ }
1755
+ }
1756
+ return null;
1757
+ }
1758
+
1186
1759
  function handleSignal() {
1187
1760
  runtime.stopping = true;
1188
1761
  }
@@ -1294,7 +1867,7 @@ function isCodeEventEntry(raw) {
1294
1867
  return false;
1295
1868
  }
1296
1869
  const fileEventType = normalizeTimelineFileEventType(raw.fileEventType ?? "");
1297
- return fileEventType === "write" || fileEventType === "create";
1870
+ return ["write", "create", "delete", "rename"].includes(fileEventType);
1298
1871
  }
1299
1872
 
1300
1873
  function normalizeCodeEvents(rawItems, maxItems) {
@@ -1377,6 +1950,7 @@ function normalizeTimelineEntry(raw) {
1377
1950
  summary,
1378
1951
  messageText,
1379
1952
  fileEventType,
1953
+ previousFileRefs: normalizeTimelineFileRefs(raw.previousFileRefs ?? []),
1380
1954
  imagePaths: normalizeTimelineImagePaths(raw.imagePaths ?? raw.localImagePaths ?? []),
1381
1955
  fileRefs: normalizeTimelineFileRefs(raw.fileRefs ?? extractTimelineFileRefs(messageText)),
1382
1956
  diffText,
@@ -1391,6 +1965,7 @@ function normalizeTimelineEntry(raw) {
1391
1965
  readOnly: raw.readOnly !== false,
1392
1966
  primaryLabel: cleanText(raw.primaryLabel ?? "") || "詳細",
1393
1967
  tone: cleanText(raw.tone ?? "") || "secondary",
1968
+ cwd: resolvePath(cleanText(raw.cwd || "")),
1394
1969
  };
1395
1970
  }
1396
1971
 
@@ -1415,6 +1990,8 @@ function recordTimelineEntry({ config, runtime, state, entry }) {
1415
1990
  item.diffAddedLines,
1416
1991
  item.diffRemovedLines,
1417
1992
  item.diffText,
1993
+ item.previousFileRefs,
1994
+ item.cwd,
1418
1995
  ])
1419
1996
  ) !==
1420
1997
  JSON.stringify(
@@ -1427,6 +2004,8 @@ function recordTimelineEntry({ config, runtime, state, entry }) {
1427
2004
  item.diffAddedLines,
1428
2005
  item.diffRemovedLines,
1429
2006
  item.diffText,
2007
+ item.previousFileRefs,
2008
+ item.cwd,
1430
2009
  ])
1431
2010
  );
1432
2011
  runtime.recentTimelineEntries = nextItems;
@@ -1458,6 +2037,8 @@ function recordCodeEvent({ config, runtime, state, entry }) {
1458
2037
  item.diffAddedLines,
1459
2038
  item.diffRemovedLines,
1460
2039
  item.diffText,
2040
+ item.previousFileRefs,
2041
+ item.cwd,
1461
2042
  ])
1462
2043
  ) !==
1463
2044
  JSON.stringify(
@@ -1470,6 +2051,8 @@ function recordCodeEvent({ config, runtime, state, entry }) {
1470
2051
  item.diffAddedLines,
1471
2052
  item.diffRemovedLines,
1472
2053
  item.diffText,
2054
+ item.previousFileRefs,
2055
+ item.cwd,
1473
2056
  ])
1474
2057
  );
1475
2058
  runtime.recentCodeEvents = nextItems;
@@ -1504,6 +2087,8 @@ function syncRecentCodeEventsFromTimeline({ config, runtime, state }) {
1504
2087
  item.diffAddedLines,
1505
2088
  item.diffRemovedLines,
1506
2089
  item.diffText,
2090
+ item.previousFileRefs,
2091
+ item.cwd,
1507
2092
  ])
1508
2093
  ) !==
1509
2094
  JSON.stringify(
@@ -1517,6 +2102,8 @@ function syncRecentCodeEventsFromTimeline({ config, runtime, state }) {
1517
2102
  item.diffAddedLines,
1518
2103
  item.diffRemovedLines,
1519
2104
  item.diffText,
2105
+ item.previousFileRefs,
2106
+ item.cwd,
1520
2107
  ])
1521
2108
  );
1522
2109
  runtime.recentCodeEvents = nextItems;
@@ -3028,7 +3615,12 @@ async function buildRolloutFileTimelineEntries({ config, record, fileState, runt
3028
3615
  }
3029
3616
 
3030
3617
  if (payloadType === "custom_tool_call_output") {
3031
- const updates = extractUpdatedFileRefsByType(payload.output ?? "");
3618
+ const storedPatch = await findStoredApplyPatchInput({
3619
+ fileState,
3620
+ callId,
3621
+ rolloutFilePath,
3622
+ });
3623
+ const updates = extractUpdatedFileRefsByType(payload.output ?? "", storedPatch?.inputText || "");
3032
3624
  const entries = [];
3033
3625
  const createDiff = await buildFileEventDiff({
3034
3626
  fileState,
@@ -3042,6 +3634,25 @@ async function buildRolloutFileTimelineEntries({ config, record, fileState, runt
3042
3634
  callId,
3043
3635
  fileRefs: updates.write,
3044
3636
  fileEventType: "write",
3637
+ previousFileRefs: [],
3638
+ rolloutFilePath,
3639
+ });
3640
+ const deleteDiff = await buildFileEventDiff({
3641
+ fileState,
3642
+ callId,
3643
+ fileRefs: updates.delete,
3644
+ fileEventType: "delete",
3645
+ previousFileRefs: [],
3646
+ rolloutFilePath,
3647
+ });
3648
+ const renameNewRefs = updates.rename.map((entry) => entry.newFileRef);
3649
+ const renameOldRefs = updates.rename.map((entry) => entry.oldFileRef);
3650
+ const renameDiff = await buildFileEventDiff({
3651
+ fileState,
3652
+ callId,
3653
+ fileRefs: renameNewRefs,
3654
+ fileEventType: "rename",
3655
+ previousFileRefs: renameOldRefs,
3045
3656
  rolloutFilePath,
3046
3657
  });
3047
3658
 
@@ -3063,6 +3674,7 @@ async function buildRolloutFileTimelineEntries({ config, record, fileState, runt
3063
3674
  diffAddedLines: createDiff.diffAddedLines,
3064
3675
  diffRemovedLines: createDiff.diffRemovedLines,
3065
3676
  createdAtMs,
3677
+ cwd: fileState.cwd || "",
3066
3678
  readOnly: true,
3067
3679
  })
3068
3680
  );
@@ -3086,6 +3698,56 @@ async function buildRolloutFileTimelineEntries({ config, record, fileState, runt
3086
3698
  diffAddedLines: writeDiff.diffAddedLines,
3087
3699
  diffRemovedLines: writeDiff.diffRemovedLines,
3088
3700
  createdAtMs,
3701
+ cwd: fileState.cwd || "",
3702
+ readOnly: true,
3703
+ })
3704
+ );
3705
+ }
3706
+
3707
+ if (updates.delete.length > 0) {
3708
+ entries.push(
3709
+ normalizeTimelineEntry({
3710
+ stableId: `file_event:delete:${threadId}:${callId || historyToken(`${threadId}:${createdAtMs}:${updates.delete.join("|")}`)}`,
3711
+ token: historyToken(`file_event:delete:${threadId}:${callId || createdAtMs}`),
3712
+ kind: "file_event",
3713
+ fileEventType: "delete",
3714
+ threadId,
3715
+ threadLabel,
3716
+ title: fileEventTitle(DEFAULT_LOCALE, "delete"),
3717
+ summary: "",
3718
+ fileRefs: updates.delete,
3719
+ diffText: deleteDiff.diffText,
3720
+ diffSource: deleteDiff.diffSource,
3721
+ diffAvailable: deleteDiff.diffAvailable,
3722
+ diffAddedLines: deleteDiff.diffAddedLines,
3723
+ diffRemovedLines: deleteDiff.diffRemovedLines,
3724
+ createdAtMs,
3725
+ cwd: fileState.cwd || "",
3726
+ readOnly: true,
3727
+ })
3728
+ );
3729
+ }
3730
+
3731
+ if (updates.rename.length > 0) {
3732
+ entries.push(
3733
+ normalizeTimelineEntry({
3734
+ stableId: `file_event:rename:${threadId}:${callId || historyToken(`${threadId}:${createdAtMs}:${renameOldRefs.join("|")}=>${renameNewRefs.join("|")}`)}`,
3735
+ token: historyToken(`file_event:rename:${threadId}:${callId || createdAtMs}`),
3736
+ kind: "file_event",
3737
+ fileEventType: "rename",
3738
+ threadId,
3739
+ threadLabel,
3740
+ title: fileEventTitle(DEFAULT_LOCALE, "rename"),
3741
+ summary: "",
3742
+ fileRefs: renameNewRefs,
3743
+ previousFileRefs: renameOldRefs,
3744
+ diffText: renameDiff.diffText,
3745
+ diffSource: renameDiff.diffSource,
3746
+ diffAvailable: renameDiff.diffAvailable,
3747
+ diffAddedLines: renameDiff.diffAddedLines,
3748
+ diffRemovedLines: renameDiff.diffRemovedLines,
3749
+ createdAtMs,
3750
+ cwd: fileState.cwd || "",
3089
3751
  readOnly: true,
3090
3752
  })
3091
3753
  );
@@ -6158,16 +6820,16 @@ function formatNativeApprovalMessage(kind, params, locale = config?.defaultLocal
6158
6820
  function formatCommandApprovalMessage(params, locale = config?.defaultLocale || DEFAULT_LOCALE) {
6159
6821
  const parts = [];
6160
6822
  const reason = truncate(cleanText(params.reason ?? params.justification ?? ""), 220);
6161
- const command = truncate(cleanText(params.command ?? params.cmd ?? ""), 220);
6823
+ const command = truncate(cleanText(params.command ?? params.cmd ?? ""), 1200);
6162
6824
  if (reason) {
6163
6825
  parts.push(reason);
6164
6826
  } else {
6165
6827
  parts.push(t(locale, "server.message.commandApprovalNeeded"));
6166
6828
  }
6167
6829
  if (command) {
6168
- parts.push(t(locale, "server.message.commandPrefix", { command }));
6830
+ parts.push(`${t(locale, "server.message.commandLabel")}\n\`\`\`sh\n${command}\n\`\`\``);
6169
6831
  }
6170
- return truncate(parts.join("\n") || t(locale, "server.message.commandApprovalNeeded"), 1024);
6832
+ return parts.join("\n\n") || t(locale, "server.message.commandApprovalNeeded");
6171
6833
  }
6172
6834
 
6173
6835
  function formatFileApprovalMessage(params, locale = config?.defaultLocale || DEFAULT_LOCALE) {
@@ -7759,8 +8421,8 @@ function buildCompletedInboxItems(runtime, state, config, locale) {
7759
8421
  }));
7760
8422
  }
7761
8423
 
7762
- function buildDiffInboxItems(runtime, state, config, locale) {
7763
- return buildDiffThreadGroups(runtime, state, config).map((group) => ({
8424
+ async function buildDiffInboxItems(runtime, state, config, locale) {
8425
+ return (await buildDiffThreadGroups(runtime, state, config)).map((group) => ({
7764
8426
  kind: "diff_thread",
7765
8427
  token: group.token,
7766
8428
  threadId: group.threadId,
@@ -7774,6 +8436,14 @@ function buildDiffInboxItems(runtime, state, config, locale) {
7774
8436
  latestChangeFileRefs: normalizeTimelineFileRefs(group.latestChangeFileRefs ?? []),
7775
8437
  diffAddedLines: group.diffAddedLines,
7776
8438
  diffRemovedLines: group.diffRemovedLines,
8439
+ files: Array.isArray(group.files)
8440
+ ? group.files.map((fileGroup) => ({
8441
+ fileRef: cleanTimelineFileRef(fileGroup.fileRef || ""),
8442
+ oldFileRef: cleanTimelineFileRef(fileGroup.oldFileRef || ""),
8443
+ newFileRef: cleanTimelineFileRef(fileGroup.newFileRef || ""),
8444
+ changeType: normalizeTimelineFileEventType(fileGroup.changeType || ""),
8445
+ }))
8446
+ : [],
7777
8447
  primaryLabel: t(locale, "server.action.detail"),
7778
8448
  createdAtMs: group.latestChangedAtMs,
7779
8449
  }));
@@ -7785,116 +8455,144 @@ function diffThreadToken(threadId, threadLabel = "") {
7785
8455
  return historyToken(`diff_thread:${normalizedThreadId || normalizedThreadLabel || "unknown"}`);
7786
8456
  }
7787
8457
 
7788
- function buildDiffThreadGroups(runtime, state, config) {
8458
+ async function buildDiffThreadGroups(runtime, state, config) {
7789
8459
  const items = normalizeCodeEvents(
7790
8460
  state.recentCodeEvents ?? runtime.recentCodeEvents,
7791
8461
  config.maxCodeEvents
7792
8462
  );
7793
8463
  runtime.recentCodeEvents = items;
7794
8464
 
8465
+ const repoRootCache = new Map();
7795
8466
  const relevantItems = items
7796
8467
  .slice()
7797
- .sort((left, right) => Number(left.createdAtMs ?? 0) - Number(right.createdAtMs ?? 0));
7798
-
7799
- const groupsByThread = new Map();
8468
+ .sort((left, right) => Number(right.createdAtMs ?? 0) - Number(left.createdAtMs ?? 0));
8469
+ const candidatesByRepoRoot = new Map();
7800
8470
 
7801
8471
  for (const item of relevantItems) {
7802
- const threadId = cleanText(item.threadId || "");
7803
- const threadLabel = cleanText(item.threadLabel || "");
7804
- const threadKey = threadId || `unknown:${threadLabel || item.token}`;
7805
- const fileRefs = normalizeTimelineFileRefs(item.fileRefs ?? []);
7806
- if (fileRefs.length === 0) {
8472
+ const repoRoot = await resolveCodeEventRepoRoot(item, repoRootCache);
8473
+ if (!repoRoot) {
7807
8474
  continue;
7808
8475
  }
7809
-
7810
- let threadGroup = groupsByThread.get(threadKey);
7811
- if (!threadGroup) {
7812
- threadGroup = {
7813
- kind: "diff_thread",
7814
- token: diffThreadToken(threadId, threadLabel),
7815
- threadId,
7816
- threadLabel,
7817
- changedFileCount: 0,
7818
- latestChangedAtMs: 0,
7819
- latestChangedAtMsForSummary: 0,
7820
- latestChangeType: "",
7821
- latestChangeFileRefs: [],
7822
- diffAddedLines: 0,
7823
- diffRemovedLines: 0,
7824
- filesByRef: new Map(),
7825
- };
7826
- groupsByThread.set(threadKey, threadGroup);
7827
- } else if (!threadGroup.threadLabel && threadLabel) {
7828
- threadGroup.threadLabel = threadLabel;
8476
+ const candidates = expandCodeEventOwnershipCandidates(item, repoRoot);
8477
+ if (candidates.length === 0) {
8478
+ continue;
7829
8479
  }
8480
+ const existing = candidatesByRepoRoot.get(repoRoot) ?? [];
8481
+ existing.push(...candidates);
8482
+ candidatesByRepoRoot.set(repoRoot, existing);
8483
+ }
7830
8484
 
7831
- const eventFileEventType = normalizeTimelineFileEventType(item.fileEventType ?? "");
7832
- const splitSections = splitUnifiedDiffTextByFile(item.diffText);
8485
+ const groupsByThread = new Map();
7833
8486
 
7834
- for (const fileRef of fileRefs) {
7835
- const normalizedFileRef = cleanTimelineFileRef(fileRef);
7836
- if (!normalizedFileRef) {
8487
+ for (const [repoRoot, candidates] of candidatesByRepoRoot.entries()) {
8488
+ const currentChanges = await buildCurrentUnstagedChangesForRepo({ repoRoot, candidates });
8489
+ for (const change of currentChanges) {
8490
+ const owner = resolveCurrentChangeOwner(change, candidates);
8491
+ if (!owner) {
7837
8492
  continue;
7838
8493
  }
7839
8494
 
7840
- let sectionDiffText = "";
7841
- const matchingSection = splitSections.find((section) => timelineFileRefsMatch(section.fileRef, normalizedFileRef));
7842
- if (matchingSection?.diffText) {
7843
- sectionDiffText = matchingSection.diffText;
7844
- } else if (fileRefs.length === 1) {
7845
- sectionDiffText = normalizeTimelineDiffText(item.diffText);
8495
+ const threadId = cleanText(owner.threadId || "");
8496
+ const threadLabel = cleanText(owner.threadLabel || "");
8497
+ const threadKey = threadId || `unknown:${threadLabel || cleanText(change.fileRef || change.newFileRef || change.oldFileRef || "")}`;
8498
+ let threadGroup = groupsByThread.get(threadKey);
8499
+ if (!threadGroup) {
8500
+ threadGroup = {
8501
+ kind: "diff_thread",
8502
+ token: diffThreadToken(threadId, threadLabel),
8503
+ threadId,
8504
+ threadLabel,
8505
+ changedFileCount: 0,
8506
+ latestChangedAtMs: 0,
8507
+ latestChangedAtMsForSummary: 0,
8508
+ latestChangeType: "",
8509
+ latestChangeFileRefs: [],
8510
+ diffAddedLines: 0,
8511
+ diffRemovedLines: 0,
8512
+ filesByRef: new Map(),
8513
+ };
8514
+ groupsByThread.set(threadKey, threadGroup);
8515
+ } else if (!threadGroup.threadLabel && threadLabel) {
8516
+ threadGroup.threadLabel = threadLabel;
7846
8517
  }
7847
8518
 
7848
- const sectionCounts = diffLineCounts(sectionDiffText);
7849
- const sectionAvailable = Boolean(sectionDiffText);
8519
+ const fileGroupKey =
8520
+ normalizeTimelineFileEventType(change.changeType) === "rename"
8521
+ ? codeOwnershipKeyForRename(change.oldFileRef || "", change.newFileRef || change.fileRef || "")
8522
+ : `${normalizeTimelineFileEventType(change.changeType)}:${cleanTimelineFileRef(change.fileRef || change.newFileRef || change.oldFileRef || "")}`;
8523
+ if (!fileGroupKey) {
8524
+ continue;
8525
+ }
7850
8526
 
7851
- let fileGroup = threadGroup.filesByRef.get(normalizedFileRef);
8527
+ const latestOwnerChangeAtMs = Number(owner.createdAtMs) || 0;
8528
+ const normalizedFileRef = cleanTimelineFileRef(change.fileRef || change.newFileRef || change.oldFileRef || "");
8529
+ const normalizedOldFileRef = cleanTimelineFileRef(change.oldFileRef || "");
8530
+ const normalizedNewFileRef = cleanTimelineFileRef(change.newFileRef || normalizedFileRef);
8531
+ let fileGroup = threadGroup.filesByRef.get(fileGroupKey);
7852
8532
  if (!fileGroup) {
7853
8533
  fileGroup = {
7854
8534
  fileRef: normalizedFileRef,
7855
- fileLabel: path.basename(normalizedFileRef) || normalizedFileRef,
8535
+ oldFileRef: normalizedOldFileRef,
8536
+ newFileRef: normalizedNewFileRef,
8537
+ fileLabel:
8538
+ path.basename(
8539
+ normalizeTimelineFileEventType(change.changeType) === "delete"
8540
+ ? normalizedOldFileRef || normalizedFileRef
8541
+ : normalizedNewFileRef || normalizedFileRef
8542
+ ) ||
8543
+ normalizedNewFileRef ||
8544
+ normalizedOldFileRef ||
8545
+ normalizedFileRef,
8546
+ changeType: normalizeTimelineFileEventType(change.changeType),
7856
8547
  fileEventTypes: new Set(),
7857
8548
  addedLines: 0,
7858
8549
  removedLines: 0,
7859
- latestChangedAtMs: 0,
8550
+ latestChangedAtMs: latestOwnerChangeAtMs,
7860
8551
  sections: [],
7861
8552
  };
7862
- threadGroup.filesByRef.set(normalizedFileRef, fileGroup);
8553
+ threadGroup.filesByRef.set(fileGroupKey, fileGroup);
7863
8554
  }
7864
8555
 
7865
- if (eventFileEventType) {
7866
- fileGroup.fileEventTypes.add(eventFileEventType);
8556
+ const changeType = normalizeTimelineFileEventType(change.changeType);
8557
+ if (changeType) {
8558
+ fileGroup.fileEventTypes.add(changeType);
7867
8559
  }
7868
- fileGroup.sections.push({
7869
- createdAtMs: Number(item.createdAtMs) || 0,
7870
- diffText: sectionDiffText,
7871
- diffAvailable: sectionAvailable,
7872
- diffSource: normalizeTimelineDiffSource(item.diffSource ?? ""),
7873
- addedLines: sectionCounts.addedLines,
7874
- removedLines: sectionCounts.removedLines,
7875
- fileEventType: eventFileEventType,
7876
- });
7877
- fileGroup.addedLines += sectionCounts.addedLines;
7878
- fileGroup.removedLines += sectionCounts.removedLines;
7879
- fileGroup.latestChangedAtMs = Math.max(fileGroup.latestChangedAtMs, Number(item.createdAtMs) || 0);
7880
-
7881
- const itemCreatedAtMs = Number(item.createdAtMs) || 0;
7882
- threadGroup.latestChangedAtMs = Math.max(threadGroup.latestChangedAtMs, itemCreatedAtMs);
7883
- threadGroup.diffAddedLines += sectionCounts.addedLines;
7884
- threadGroup.diffRemovedLines += sectionCounts.removedLines;
7885
-
7886
- if (itemCreatedAtMs > threadGroup.latestChangedAtMsForSummary) {
7887
- threadGroup.latestChangedAtMsForSummary = itemCreatedAtMs;
7888
- threadGroup.latestChangeType = eventFileEventType || "";
7889
- threadGroup.latestChangeFileRefs = [normalizedFileRef];
7890
- } else if (itemCreatedAtMs === (threadGroup.latestChangedAtMsForSummary || 0)) {
7891
- if (eventFileEventType && threadGroup.latestChangeType && threadGroup.latestChangeType !== eventFileEventType) {
8560
+ fileGroup.changeType = changeType || fileGroup.changeType;
8561
+ fileGroup.addedLines = Math.max(0, Number(change.diffAddedLines) || 0);
8562
+ fileGroup.removedLines = Math.max(0, Number(change.diffRemovedLines) || 0);
8563
+ fileGroup.latestChangedAtMs = latestOwnerChangeAtMs;
8564
+ fileGroup.fileRef = normalizedFileRef;
8565
+ fileGroup.oldFileRef = normalizedOldFileRef;
8566
+ fileGroup.newFileRef = normalizedNewFileRef;
8567
+ fileGroup.sections = [
8568
+ {
8569
+ createdAtMs: latestOwnerChangeAtMs,
8570
+ diffText: normalizeTimelineDiffText(change.diffText ?? ""),
8571
+ diffAvailable: change.diffAvailable === true || Boolean(change.diffText),
8572
+ diffSource: normalizeTimelineDiffSource(change.diffSource ?? ""),
8573
+ addedLines: Math.max(0, Number(change.diffAddedLines) || 0),
8574
+ removedLines: Math.max(0, Number(change.diffRemovedLines) || 0),
8575
+ fileEventType: changeType,
8576
+ },
8577
+ ];
8578
+
8579
+ threadGroup.latestChangedAtMs = Math.max(threadGroup.latestChangedAtMs, latestOwnerChangeAtMs);
8580
+ threadGroup.diffAddedLines += Math.max(0, Number(change.diffAddedLines) || 0);
8581
+ threadGroup.diffRemovedLines += Math.max(0, Number(change.diffRemovedLines) || 0);
8582
+
8583
+ if (latestOwnerChangeAtMs > threadGroup.latestChangedAtMsForSummary) {
8584
+ threadGroup.latestChangedAtMsForSummary = latestOwnerChangeAtMs;
8585
+ threadGroup.latestChangeType = changeType || "";
8586
+ threadGroup.latestChangeFileRefs = [normalizedFileRef || normalizedNewFileRef || normalizedOldFileRef];
8587
+ } else if (latestOwnerChangeAtMs === (threadGroup.latestChangedAtMsForSummary || 0)) {
8588
+ if (changeType && threadGroup.latestChangeType && threadGroup.latestChangeType !== changeType) {
7892
8589
  threadGroup.latestChangeType = "";
7893
- } else if (!threadGroup.latestChangeType && eventFileEventType && threadGroup.latestChangeFileRefs.length === 0) {
7894
- threadGroup.latestChangeType = eventFileEventType;
8590
+ } else if (!threadGroup.latestChangeType && changeType && threadGroup.latestChangeFileRefs.length === 0) {
8591
+ threadGroup.latestChangeType = changeType;
7895
8592
  }
7896
- if (!threadGroup.latestChangeFileRefs.includes(normalizedFileRef)) {
7897
- threadGroup.latestChangeFileRefs.push(normalizedFileRef);
8593
+ const latestFileRef = normalizedFileRef || normalizedNewFileRef || normalizedOldFileRef;
8594
+ if (latestFileRef && !threadGroup.latestChangeFileRefs.includes(latestFileRef)) {
8595
+ threadGroup.latestChangeFileRefs.push(latestFileRef);
7898
8596
  }
7899
8597
  }
7900
8598
  }
@@ -7905,12 +8603,15 @@ function buildDiffThreadGroups(runtime, state, config) {
7905
8603
  const files = [...group.filesByRef.values()]
7906
8604
  .map((fileGroup) => ({
7907
8605
  fileRef: fileGroup.fileRef,
8606
+ oldFileRef: fileGroup.oldFileRef,
8607
+ newFileRef: fileGroup.newFileRef,
7908
8608
  fileLabel: fileGroup.fileLabel,
8609
+ changeType: normalizeTimelineFileEventType(fileGroup.changeType),
7909
8610
  fileEventTypes: [...fileGroup.fileEventTypes.values()],
7910
8611
  addedLines: fileGroup.addedLines,
7911
8612
  removedLines: fileGroup.removedLines,
7912
8613
  latestChangedAtMs: fileGroup.latestChangedAtMs,
7913
- sections: fileGroup.sections.sort((left, right) => Number(left.createdAtMs ?? 0) - Number(right.createdAtMs ?? 0)),
8614
+ sections: fileGroup.sections,
7914
8615
  }))
7915
8616
  .sort((left, right) => Number(right.latestChangedAtMs ?? 0) - Number(left.latestChangedAtMs ?? 0));
7916
8617
 
@@ -7932,10 +8633,10 @@ function buildDiffThreadGroups(runtime, state, config) {
7932
8633
  .sort((left, right) => Number(right.latestChangedAtMs ?? 0) - Number(left.latestChangedAtMs ?? 0));
7933
8634
  }
7934
8635
 
7935
- function buildInboxResponse(runtime, state, config, locale) {
8636
+ async function buildInboxResponse(runtime, state, config, locale) {
7936
8637
  return {
7937
8638
  pending: buildPendingInboxItems(runtime, state, config, locale),
7938
- diff: buildDiffInboxItems(runtime, state, config, locale),
8639
+ diff: await buildDiffInboxItems(runtime, state, config, locale),
7939
8640
  completed: buildCompletedInboxItems(runtime, state, config, locale),
7940
8641
  };
7941
8642
  }
@@ -8129,6 +8830,7 @@ function buildPendingApprovalDetail(runtime, approval, locale) {
8129
8830
  const previousContext = buildPreviousApprovalContext(runtime, approval);
8130
8831
  return {
8131
8832
  kind: "approval",
8833
+ approvalKind: cleanText(approval.kind || ""),
8132
8834
  token: approval.token,
8133
8835
  title: formatLocalizedTitle(locale, "server.title.approval", approval.threadLabel),
8134
8836
  threadLabel: approval.threadLabel || "",
@@ -8431,6 +9133,7 @@ function buildTimelineFileEventDetail(entry, locale) {
8431
9133
  createdAtMs: Number(entry.createdAtMs) || 0,
8432
9134
  messageHtml: renderMessageHtml(fileEventDetailCopy(locale, fileEventType), `<p>${escapeHtml(t(locale, "detail.detailUnavailable"))}</p>`),
8433
9135
  fileRefs: normalizeTimelineFileRefs(entry.fileRefs ?? []),
9136
+ previousFileRefs: normalizeTimelineFileRefs(entry.previousFileRefs ?? []),
8434
9137
  diffAvailable: Boolean(entry.diffAvailable),
8435
9138
  diffText: normalizeTimelineDiffText(entry.diffText ?? ""),
8436
9139
  diffSource: normalizeTimelineDiffSource(entry.diffSource ?? ""),
@@ -8459,7 +9162,10 @@ function buildDiffThreadDetail(group, locale) {
8459
9162
  files: Array.isArray(group.files)
8460
9163
  ? group.files.map((fileGroup) => ({
8461
9164
  fileRef: cleanTimelineFileRef(fileGroup.fileRef),
9165
+ oldFileRef: cleanTimelineFileRef(fileGroup.oldFileRef || ""),
9166
+ newFileRef: cleanTimelineFileRef(fileGroup.newFileRef || fileGroup.fileRef || ""),
8462
9167
  fileLabel: cleanText(fileGroup.fileLabel || "") || path.basename(cleanTimelineFileRef(fileGroup.fileRef)) || cleanTimelineFileRef(fileGroup.fileRef),
9168
+ changeType: normalizeTimelineFileEventType(fileGroup.changeType ?? ""),
8463
9169
  fileEventTypes: Array.isArray(fileGroup.fileEventTypes)
8464
9170
  ? fileGroup.fileEventTypes.map((value) => normalizeTimelineFileEventType(value)).filter(Boolean)
8465
9171
  : [],
@@ -9021,9 +9727,9 @@ async function handleNativeApprovalDecision({ config, runtime, state, approval,
9021
9727
  console.log(`[native-decision] ${approval.requestKey} | ${decision}`);
9022
9728
  }
9023
9729
 
9024
- function buildApiItemDetail({ config, runtime, state, kind, token, locale }) {
9730
+ async function buildApiItemDetail({ config, runtime, state, kind, token, locale }) {
9025
9731
  if (kind === "diff_thread") {
9026
- const group = buildDiffThreadGroups(runtime, state, config).find((entry) => entry.token === token);
9732
+ const group = (await buildDiffThreadGroups(runtime, state, config)).find((entry) => entry.token === token);
9027
9733
  return group ? buildDiffThreadDetail(group, locale) : null;
9028
9734
  }
9029
9735
  if (kind === "file_event") {
@@ -9551,7 +10257,7 @@ function createNativeApprovalServer({ config, runtime, state }) {
9551
10257
  return;
9552
10258
  }
9553
10259
  const locale = resolveDeviceLocaleInfo(config, state, session.deviceId).locale;
9554
- return writeJson(res, 200, buildInboxResponse(runtime, state, config, locale));
10260
+ return writeJson(res, 200, await buildInboxResponse(runtime, state, config, locale));
9555
10261
  }
9556
10262
 
9557
10263
  if (url.pathname === "/api/timeline" && req.method === "GET") {
@@ -9600,7 +10306,7 @@ function createNativeApprovalServer({ config, runtime, state }) {
9600
10306
  const kind = decodeURIComponent(apiItemMatch[1]);
9601
10307
  const token = decodeURIComponent(apiItemMatch[2]);
9602
10308
  const locale = resolveDeviceLocaleInfo(config, state, session.deviceId).locale;
9603
- const detail = buildApiItemDetail({ config, runtime, state, kind, token, locale });
10309
+ const detail = await buildApiItemDetail({ config, runtime, state, kind, token, locale });
9604
10310
  if (!detail) {
9605
10311
  return writeJson(res, 404, { error: "item-not-found" });
9606
10312
  }