viveworker 0.1.10 → 0.1.11

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "viveworker",
3
- "version": "0.1.10",
3
+ "version": "0.1.11",
4
4
  "description": "Local device companion for Codex Desktop approvals, plan checks, questions, and notifications on your LAN.",
5
5
  "author": "Yuta Hoshino <hoshino.lireneo@gmail.com>",
6
6
  "license": "MIT",
@@ -328,6 +328,17 @@ function fileEventDetailCopy(locale, fileEventType) {
328
328
  }
329
329
  }
330
330
 
331
+ function diffThreadDetailCopy(locale, sourceMode, count) {
332
+ switch (cleanText(sourceMode || "")) {
333
+ case "git_current":
334
+ return t(locale, "detail.diffThread.copy.current", { count });
335
+ case "non_git_latest":
336
+ return t(locale, "detail.diffThread.copy.latest", { count });
337
+ default:
338
+ return t(locale, "detail.diffThread.copy", { count });
339
+ }
340
+ }
341
+
331
342
  function inferTimelineOutcome(kind, summary = "", messageText = "") {
332
343
  const normalizedKind = cleanText(kind || "");
333
344
  if (!normalizedKind) {
@@ -1764,6 +1775,205 @@ function resolveCurrentChangeOwner(change, candidates) {
1764
1775
  return null;
1765
1776
  }
1766
1777
 
1778
+ function normalizeCodeFallbackFileRef({ fileRef, cwd = "" }) {
1779
+ const normalizedFileRef = cleanTimelineFileRef(fileRef);
1780
+ if (!normalizedFileRef) {
1781
+ return "";
1782
+ }
1783
+ if (path.isAbsolute(normalizedFileRef)) {
1784
+ return resolvePath(normalizedFileRef);
1785
+ }
1786
+ const normalizedCwd = resolvePath(cleanText(cwd || ""));
1787
+ if (!normalizedCwd) {
1788
+ return normalizedFileRef;
1789
+ }
1790
+ return resolvePath(path.resolve(normalizedCwd, normalizedFileRef));
1791
+ }
1792
+
1793
+ function buildNonGitCodeChangeEntries(item) {
1794
+ const changeType = normalizeTimelineFileEventType(item?.fileEventType || "");
1795
+ if (!["write", "create", "delete", "rename"].includes(changeType)) {
1796
+ return [];
1797
+ }
1798
+
1799
+ const normalizedCwd = cleanText(item?.cwd || "");
1800
+ const normalizedFileRefs = normalizeTimelineFileRefs(item?.fileRefs ?? []).map((fileRef) =>
1801
+ normalizeCodeFallbackFileRef({ fileRef, cwd: normalizedCwd })
1802
+ );
1803
+ const normalizedPreviousFileRefs = normalizeTimelineFileRefs(item?.previousFileRefs ?? []).map((fileRef) =>
1804
+ normalizeCodeFallbackFileRef({ fileRef, cwd: normalizedCwd })
1805
+ );
1806
+ const sections = splitUnifiedDiffTextByFile(item?.diffText ?? "");
1807
+ const rawChanges = [];
1808
+
1809
+ if (changeType === "rename") {
1810
+ const pairCount = Math.max(normalizedFileRefs.length, normalizedPreviousFileRefs.length);
1811
+ for (let index = 0; index < pairCount; index += 1) {
1812
+ const newFileRef = cleanTimelineFileRef(normalizedFileRefs[index] || "");
1813
+ const oldFileRef = cleanTimelineFileRef(normalizedPreviousFileRefs[index] || "");
1814
+ if (!newFileRef && !oldFileRef) {
1815
+ continue;
1816
+ }
1817
+ rawChanges.push({
1818
+ changeType,
1819
+ fileRef: newFileRef || oldFileRef,
1820
+ oldFileRef,
1821
+ newFileRef: newFileRef || oldFileRef,
1822
+ });
1823
+ }
1824
+ } else {
1825
+ const targetRefs =
1826
+ normalizedFileRefs.length > 0
1827
+ ? normalizedFileRefs
1828
+ : changeType === "delete"
1829
+ ? normalizedPreviousFileRefs
1830
+ : [];
1831
+ for (const fileRef of targetRefs) {
1832
+ const normalizedFileRef = cleanTimelineFileRef(fileRef || "");
1833
+ if (!normalizedFileRef) {
1834
+ continue;
1835
+ }
1836
+ rawChanges.push({
1837
+ changeType,
1838
+ fileRef: normalizedFileRef,
1839
+ oldFileRef: changeType === "delete" ? normalizedFileRef : "",
1840
+ newFileRef: changeType === "delete" ? "" : normalizedFileRef,
1841
+ });
1842
+ }
1843
+ }
1844
+
1845
+ const allowWholeItemDiff = sections.length === 0 && rawChanges.length <= 1;
1846
+ const baseDiffSource = normalizeTimelineDiffSource(item?.diffSource ?? "");
1847
+ const baseAddedLines = Math.max(0, Number(item?.diffAddedLines) || 0);
1848
+ const baseRemovedLines = Math.max(0, Number(item?.diffRemovedLines) || 0);
1849
+ const threadId = cleanText(item?.threadId || "");
1850
+ const threadLabel = cleanText(item?.threadLabel || "");
1851
+ const createdAtMs = Number(item?.createdAtMs) || 0;
1852
+
1853
+ return rawChanges.map((change) => {
1854
+ const section = findMatchingCurrentDiffSection(sections, change);
1855
+ const diffText = normalizeTimelineDiffText(section?.diffText || (allowWholeItemDiff ? item?.diffText ?? "" : ""));
1856
+ const counts = diffText
1857
+ ? diffLineCounts(diffText)
1858
+ : {
1859
+ addedLines: allowWholeItemDiff ? baseAddedLines : 0,
1860
+ removedLines: allowWholeItemDiff ? baseRemovedLines : 0,
1861
+ };
1862
+ return {
1863
+ threadId,
1864
+ threadLabel,
1865
+ createdAtMs,
1866
+ changeType,
1867
+ fileRef: cleanTimelineFileRef(change.fileRef || change.newFileRef || change.oldFileRef || ""),
1868
+ oldFileRef: cleanTimelineFileRef(change.oldFileRef || ""),
1869
+ newFileRef: cleanTimelineFileRef(change.newFileRef || change.fileRef || ""),
1870
+ diffText,
1871
+ diffAvailable: item?.diffAvailable === true || Boolean(diffText),
1872
+ diffSource: baseDiffSource,
1873
+ diffAddedLines: counts.addedLines,
1874
+ diffRemovedLines: counts.removedLines,
1875
+ };
1876
+ });
1877
+ }
1878
+
1879
+ function nonGitCodeChangeIdentity(change) {
1880
+ const anchorFileRef = cleanTimelineFileRef(change?.newFileRef || change?.fileRef || change?.oldFileRef || "");
1881
+ return anchorFileRef ? `file:${anchorFileRef}` : "";
1882
+ }
1883
+
1884
+ function mergeDiffThreadSourceMode(currentMode, nextMode) {
1885
+ const normalizedCurrentMode = cleanText(currentMode || "");
1886
+ const normalizedNextMode = cleanText(nextMode || "");
1887
+ if (!normalizedCurrentMode) {
1888
+ return normalizedNextMode;
1889
+ }
1890
+ if (!normalizedNextMode || normalizedCurrentMode === normalizedNextMode) {
1891
+ return normalizedCurrentMode;
1892
+ }
1893
+ return "mixed";
1894
+ }
1895
+
1896
+ function ensureDiffThreadGroup(groupsByThread, { threadId = "", threadLabel = "", fallbackKey = "", sourceMode = "" }) {
1897
+ const normalizedThreadId = cleanText(threadId || "");
1898
+ const normalizedThreadLabel = cleanText(threadLabel || "");
1899
+ const normalizedFallbackKey = cleanText(fallbackKey || "");
1900
+ const threadKey = normalizedThreadId || `unknown:${normalizedThreadLabel || normalizedFallbackKey || "thread"}`;
1901
+ let threadGroup = groupsByThread.get(threadKey);
1902
+ if (!threadGroup) {
1903
+ threadGroup = {
1904
+ kind: "diff_thread",
1905
+ token: diffThreadToken(normalizedThreadId, normalizedThreadLabel),
1906
+ threadId: normalizedThreadId,
1907
+ threadLabel: normalizedThreadLabel,
1908
+ sourceMode: cleanText(sourceMode || ""),
1909
+ filesByRef: new Map(),
1910
+ };
1911
+ groupsByThread.set(threadKey, threadGroup);
1912
+ return threadGroup;
1913
+ }
1914
+
1915
+ if (!threadGroup.threadLabel && normalizedThreadLabel) {
1916
+ threadGroup.threadLabel = normalizedThreadLabel;
1917
+ }
1918
+ threadGroup.sourceMode = mergeDiffThreadSourceMode(threadGroup.sourceMode, sourceMode);
1919
+ return threadGroup;
1920
+ }
1921
+
1922
+ function addChangeToDiffThreadGroup({ groupsByThread, threadId, threadLabel, fallbackKey = "", sourceMode = "", change, createdAtMs }) {
1923
+ const normalizedChangeType = normalizeTimelineFileEventType(change?.changeType || "");
1924
+ const normalizedFileRef = cleanTimelineFileRef(change?.fileRef || change?.newFileRef || change?.oldFileRef || "");
1925
+ const normalizedOldFileRef = cleanTimelineFileRef(change?.oldFileRef || "");
1926
+ const normalizedNewFileRef = cleanTimelineFileRef(change?.newFileRef || normalizedFileRef);
1927
+ if (!normalizedChangeType || !(normalizedFileRef || normalizedOldFileRef || normalizedNewFileRef)) {
1928
+ return;
1929
+ }
1930
+
1931
+ const group = ensureDiffThreadGroup(groupsByThread, {
1932
+ threadId,
1933
+ threadLabel,
1934
+ fallbackKey: normalizedFileRef || normalizedNewFileRef || normalizedOldFileRef || fallbackKey,
1935
+ sourceMode,
1936
+ });
1937
+
1938
+ const fileGroupKey = `file:${normalizedNewFileRef || normalizedFileRef || normalizedOldFileRef}`;
1939
+ if (!fileGroupKey) {
1940
+ return;
1941
+ }
1942
+
1943
+ const latestChangedAtMs = Number(createdAtMs) || 0;
1944
+ const fileGroup = {
1945
+ fileRef: normalizedFileRef || normalizedNewFileRef || normalizedOldFileRef,
1946
+ oldFileRef: normalizedOldFileRef,
1947
+ newFileRef: normalizedNewFileRef || normalizedFileRef,
1948
+ fileLabel:
1949
+ path.basename(
1950
+ normalizedChangeType === "delete"
1951
+ ? normalizedOldFileRef || normalizedFileRef
1952
+ : normalizedNewFileRef || normalizedFileRef || normalizedOldFileRef
1953
+ ) ||
1954
+ normalizedNewFileRef ||
1955
+ normalizedOldFileRef ||
1956
+ normalizedFileRef,
1957
+ changeType: normalizedChangeType,
1958
+ fileEventTypes: [normalizedChangeType],
1959
+ addedLines: Math.max(0, Number(change?.diffAddedLines) || 0),
1960
+ removedLines: Math.max(0, Number(change?.diffRemovedLines) || 0),
1961
+ latestChangedAtMs,
1962
+ sections: [
1963
+ {
1964
+ createdAtMs: latestChangedAtMs,
1965
+ diffText: normalizeTimelineDiffText(change?.diffText ?? ""),
1966
+ diffAvailable: change?.diffAvailable === true || Boolean(change?.diffText),
1967
+ diffSource: normalizeTimelineDiffSource(change?.diffSource ?? ""),
1968
+ addedLines: Math.max(0, Number(change?.diffAddedLines) || 0),
1969
+ removedLines: Math.max(0, Number(change?.diffRemovedLines) || 0),
1970
+ fileEventType: normalizedChangeType,
1971
+ },
1972
+ ],
1973
+ };
1974
+ group.filesByRef.set(fileGroupKey, fileGroup);
1975
+ }
1976
+
1767
1977
  function handleSignal() {
1768
1978
  runtime.stopping = true;
1769
1979
  }
@@ -6772,6 +6982,90 @@ function getNativeThreadLabel({ runtime, conversationId, cwd }) {
6772
6982
  return shortId(normalizedConversationId) || "Codex task";
6773
6983
  }
6774
6984
 
6985
+ function threadStateArchiveStatus(threadState) {
6986
+ if (!isPlainObject(threadState)) {
6987
+ return "";
6988
+ }
6989
+
6990
+ const booleanCandidates = [
6991
+ threadState.archived,
6992
+ threadState.isArchived,
6993
+ threadState.hidden,
6994
+ threadState.isHidden,
6995
+ threadState.deleted,
6996
+ threadState.isDeleted,
6997
+ threadState.closed,
6998
+ threadState.isClosed,
6999
+ threadState.thread?.archived,
7000
+ threadState.thread?.isArchived,
7001
+ threadState.thread?.hidden,
7002
+ threadState.thread?.isHidden,
7003
+ threadState.thread?.deleted,
7004
+ threadState.thread?.isDeleted,
7005
+ threadState.metadata?.archived,
7006
+ threadState.metadata?.isArchived,
7007
+ threadState.metadata?.hidden,
7008
+ threadState.metadata?.isHidden,
7009
+ threadState.metadata?.deleted,
7010
+ threadState.metadata?.isDeleted,
7011
+ ];
7012
+ if (booleanCandidates.some((value) => value === true)) {
7013
+ return "archived";
7014
+ }
7015
+
7016
+ const statusCandidates = [
7017
+ threadState.status,
7018
+ threadState.state,
7019
+ threadState.visibility,
7020
+ threadState.lifecycle,
7021
+ threadState.thread?.status,
7022
+ threadState.thread?.state,
7023
+ threadState.thread?.visibility,
7024
+ threadState.metadata?.status,
7025
+ threadState.metadata?.state,
7026
+ threadState.metadata?.visibility,
7027
+ ]
7028
+ .map((value) => cleanText(value || "").toLowerCase())
7029
+ .filter(Boolean);
7030
+
7031
+ for (const status of statusCandidates) {
7032
+ if (["archived", "hidden", "deleted", "closed"].includes(status)) {
7033
+ return status;
7034
+ }
7035
+ }
7036
+
7037
+ return "";
7038
+ }
7039
+
7040
+ function shouldExcludeCodeThread(runtime, threadId) {
7041
+ const normalizedThreadId = cleanText(threadId || "");
7042
+ if (!normalizedThreadId) {
7043
+ return false;
7044
+ }
7045
+
7046
+ const threadState = runtime.threadStates.get(normalizedThreadId) ?? null;
7047
+ if (threadStateArchiveStatus(threadState)) {
7048
+ return true;
7049
+ }
7050
+
7051
+ const currentSessionHasThread = Array.isArray(runtime.knownFiles)
7052
+ ? runtime.knownFiles.some((filePath) => extractThreadIdFromRolloutPath(filePath) === normalizedThreadId)
7053
+ : false;
7054
+ if (currentSessionHasThread) {
7055
+ return false;
7056
+ }
7057
+
7058
+ if (threadState) {
7059
+ return false;
7060
+ }
7061
+
7062
+ if (runtime.sessionIndex instanceof Map && runtime.sessionIndex.size > 0) {
7063
+ return runtime.sessionIndex.has(normalizedThreadId);
7064
+ }
7065
+
7066
+ return false;
7067
+ }
7068
+
6775
7069
  async function findRolloutThreadCwd(runtime, conversationId) {
6776
7070
  const normalizedConversationId = cleanText(conversationId || "");
6777
7071
  if (!normalizedConversationId) {
@@ -8447,6 +8741,7 @@ async function buildDiffInboxItems(runtime, state, config, locale) {
8447
8741
  token: group.token,
8448
8742
  threadId: group.threadId,
8449
8743
  threadLabel: group.threadLabel || "",
8744
+ sourceMode: cleanText(group.sourceMode || ""),
8450
8745
  title: cleanText(group.threadLabel || "") || kindTitle(locale, "diff_thread"),
8451
8746
  summary: t(locale, "diff.threadSummary", { count: group.changedFileCount }),
8452
8747
  changedFileCount: group.changedFileCount,
@@ -8487,10 +8782,12 @@ async function buildDiffThreadGroups(runtime, state, config) {
8487
8782
  .slice()
8488
8783
  .sort((left, right) => Number(right.createdAtMs ?? 0) - Number(left.createdAtMs ?? 0));
8489
8784
  const candidatesByRepoRoot = new Map();
8785
+ const nonGitRelevantItems = [];
8490
8786
 
8491
8787
  for (const item of relevantItems) {
8492
8788
  const repoRoot = await resolveCodeEventRepoRoot(item, repoRootCache);
8493
8789
  if (!repoRoot) {
8790
+ nonGitRelevantItems.push(item);
8494
8791
  continue;
8495
8792
  }
8496
8793
  const candidates = expandCodeEventOwnershipCandidates(item, repoRoot);
@@ -8512,112 +8809,42 @@ async function buildDiffThreadGroups(runtime, state, config) {
8512
8809
  continue;
8513
8810
  }
8514
8811
 
8515
- const threadId = cleanText(owner.threadId || "");
8516
- const threadLabel = cleanText(owner.threadLabel || "");
8517
- const threadKey = threadId || `unknown:${threadLabel || cleanText(change.fileRef || change.newFileRef || change.oldFileRef || "")}`;
8518
- let threadGroup = groupsByThread.get(threadKey);
8519
- if (!threadGroup) {
8520
- threadGroup = {
8521
- kind: "diff_thread",
8522
- token: diffThreadToken(threadId, threadLabel),
8523
- threadId,
8524
- threadLabel,
8525
- changedFileCount: 0,
8526
- latestChangedAtMs: 0,
8527
- latestChangedAtMsForSummary: 0,
8528
- latestChangeType: "",
8529
- latestChangeFileRefs: [],
8530
- diffAddedLines: 0,
8531
- diffRemovedLines: 0,
8532
- filesByRef: new Map(),
8533
- };
8534
- groupsByThread.set(threadKey, threadGroup);
8535
- } else if (!threadGroup.threadLabel && threadLabel) {
8536
- threadGroup.threadLabel = threadLabel;
8537
- }
8812
+ addChangeToDiffThreadGroup({
8813
+ groupsByThread,
8814
+ threadId: cleanText(owner.threadId || ""),
8815
+ threadLabel: cleanText(owner.threadLabel || ""),
8816
+ fallbackKey: cleanText(change.fileRef || change.newFileRef || change.oldFileRef || ""),
8817
+ sourceMode: "git_current",
8818
+ change,
8819
+ createdAtMs: Number(owner.createdAtMs) || 0,
8820
+ });
8821
+ }
8822
+ }
8538
8823
 
8539
- const fileGroupKey =
8540
- normalizeTimelineFileEventType(change.changeType) === "rename"
8541
- ? codeOwnershipKeyForRename(change.oldFileRef || "", change.newFileRef || change.fileRef || "")
8542
- : `${normalizeTimelineFileEventType(change.changeType)}:${cleanTimelineFileRef(change.fileRef || change.newFileRef || change.oldFileRef || "")}`;
8543
- if (!fileGroupKey) {
8824
+ const latestNonGitChangesByIdentity = new Map();
8825
+ for (const item of nonGitRelevantItems) {
8826
+ const changes = buildNonGitCodeChangeEntries(item);
8827
+ for (const change of changes) {
8828
+ const identity = nonGitCodeChangeIdentity(change);
8829
+ if (!identity || latestNonGitChangesByIdentity.has(identity)) {
8544
8830
  continue;
8545
8831
  }
8546
-
8547
- const latestOwnerChangeAtMs = Number(owner.createdAtMs) || 0;
8548
- const normalizedFileRef = cleanTimelineFileRef(change.fileRef || change.newFileRef || change.oldFileRef || "");
8549
- const normalizedOldFileRef = cleanTimelineFileRef(change.oldFileRef || "");
8550
- const normalizedNewFileRef = cleanTimelineFileRef(change.newFileRef || normalizedFileRef);
8551
- let fileGroup = threadGroup.filesByRef.get(fileGroupKey);
8552
- if (!fileGroup) {
8553
- fileGroup = {
8554
- fileRef: normalizedFileRef,
8555
- oldFileRef: normalizedOldFileRef,
8556
- newFileRef: normalizedNewFileRef,
8557
- fileLabel:
8558
- path.basename(
8559
- normalizeTimelineFileEventType(change.changeType) === "delete"
8560
- ? normalizedOldFileRef || normalizedFileRef
8561
- : normalizedNewFileRef || normalizedFileRef
8562
- ) ||
8563
- normalizedNewFileRef ||
8564
- normalizedOldFileRef ||
8565
- normalizedFileRef,
8566
- changeType: normalizeTimelineFileEventType(change.changeType),
8567
- fileEventTypes: new Set(),
8568
- addedLines: 0,
8569
- removedLines: 0,
8570
- latestChangedAtMs: latestOwnerChangeAtMs,
8571
- sections: [],
8572
- };
8573
- threadGroup.filesByRef.set(fileGroupKey, fileGroup);
8574
- }
8575
-
8576
- const changeType = normalizeTimelineFileEventType(change.changeType);
8577
- if (changeType) {
8578
- fileGroup.fileEventTypes.add(changeType);
8579
- }
8580
- fileGroup.changeType = changeType || fileGroup.changeType;
8581
- fileGroup.addedLines = Math.max(0, Number(change.diffAddedLines) || 0);
8582
- fileGroup.removedLines = Math.max(0, Number(change.diffRemovedLines) || 0);
8583
- fileGroup.latestChangedAtMs = latestOwnerChangeAtMs;
8584
- fileGroup.fileRef = normalizedFileRef;
8585
- fileGroup.oldFileRef = normalizedOldFileRef;
8586
- fileGroup.newFileRef = normalizedNewFileRef;
8587
- fileGroup.sections = [
8588
- {
8589
- createdAtMs: latestOwnerChangeAtMs,
8590
- diffText: normalizeTimelineDiffText(change.diffText ?? ""),
8591
- diffAvailable: change.diffAvailable === true || Boolean(change.diffText),
8592
- diffSource: normalizeTimelineDiffSource(change.diffSource ?? ""),
8593
- addedLines: Math.max(0, Number(change.diffAddedLines) || 0),
8594
- removedLines: Math.max(0, Number(change.diffRemovedLines) || 0),
8595
- fileEventType: changeType,
8596
- },
8597
- ];
8598
-
8599
- threadGroup.latestChangedAtMs = Math.max(threadGroup.latestChangedAtMs, latestOwnerChangeAtMs);
8600
- threadGroup.diffAddedLines += Math.max(0, Number(change.diffAddedLines) || 0);
8601
- threadGroup.diffRemovedLines += Math.max(0, Number(change.diffRemovedLines) || 0);
8602
-
8603
- if (latestOwnerChangeAtMs > threadGroup.latestChangedAtMsForSummary) {
8604
- threadGroup.latestChangedAtMsForSummary = latestOwnerChangeAtMs;
8605
- threadGroup.latestChangeType = changeType || "";
8606
- threadGroup.latestChangeFileRefs = [normalizedFileRef || normalizedNewFileRef || normalizedOldFileRef];
8607
- } else if (latestOwnerChangeAtMs === (threadGroup.latestChangedAtMsForSummary || 0)) {
8608
- if (changeType && threadGroup.latestChangeType && threadGroup.latestChangeType !== changeType) {
8609
- threadGroup.latestChangeType = "";
8610
- } else if (!threadGroup.latestChangeType && changeType && threadGroup.latestChangeFileRefs.length === 0) {
8611
- threadGroup.latestChangeType = changeType;
8612
- }
8613
- const latestFileRef = normalizedFileRef || normalizedNewFileRef || normalizedOldFileRef;
8614
- if (latestFileRef && !threadGroup.latestChangeFileRefs.includes(latestFileRef)) {
8615
- threadGroup.latestChangeFileRefs.push(latestFileRef);
8616
- }
8617
- }
8832
+ latestNonGitChangesByIdentity.set(identity, change);
8618
8833
  }
8619
8834
  }
8620
8835
 
8836
+ for (const change of latestNonGitChangesByIdentity.values()) {
8837
+ addChangeToDiffThreadGroup({
8838
+ groupsByThread,
8839
+ threadId: cleanText(change.threadId || ""),
8840
+ threadLabel: cleanText(change.threadLabel || ""),
8841
+ fallbackKey: cleanText(change.fileRef || change.newFileRef || change.oldFileRef || ""),
8842
+ sourceMode: "non_git_latest",
8843
+ change,
8844
+ createdAtMs: Number(change.createdAtMs) || 0,
8845
+ });
8846
+ }
8847
+
8621
8848
  return [...groupsByThread.values()]
8622
8849
  .map((group) => {
8623
8850
  const files = [...group.filesByRef.values()]
@@ -8627,7 +8854,9 @@ async function buildDiffThreadGroups(runtime, state, config) {
8627
8854
  newFileRef: fileGroup.newFileRef,
8628
8855
  fileLabel: fileGroup.fileLabel,
8629
8856
  changeType: normalizeTimelineFileEventType(fileGroup.changeType),
8630
- fileEventTypes: [...fileGroup.fileEventTypes.values()],
8857
+ fileEventTypes: Array.isArray(fileGroup.fileEventTypes)
8858
+ ? fileGroup.fileEventTypes
8859
+ : [...fileGroup.fileEventTypes.values()],
8631
8860
  addedLines: fileGroup.addedLines,
8632
8861
  removedLines: fileGroup.removedLines,
8633
8862
  latestChangedAtMs: fileGroup.latestChangedAtMs,
@@ -8635,20 +8864,33 @@ async function buildDiffThreadGroups(runtime, state, config) {
8635
8864
  }))
8636
8865
  .sort((left, right) => Number(right.latestChangedAtMs ?? 0) - Number(left.latestChangedAtMs ?? 0));
8637
8866
 
8867
+ const latestChangedAtMs = Math.max(0, ...files.map((fileGroup) => Number(fileGroup.latestChangedAtMs) || 0));
8868
+ const latestFiles = files.filter((fileGroup) => Number(fileGroup.latestChangedAtMs || 0) === latestChangedAtMs);
8869
+ const latestChangeTypes = [...new Set(latestFiles.map((fileGroup) => normalizeTimelineFileEventType(fileGroup.changeType)).filter(Boolean))];
8870
+ const latestChangeFileRefs = normalizeTimelineFileRefs(
8871
+ latestFiles
8872
+ .map((fileGroup) => cleanTimelineFileRef(fileGroup.fileRef || fileGroup.newFileRef || fileGroup.oldFileRef || ""))
8873
+ .filter(Boolean)
8874
+ );
8875
+ const diffAddedLines = files.reduce((sum, fileGroup) => sum + Math.max(0, Number(fileGroup.addedLines) || 0), 0);
8876
+ const diffRemovedLines = files.reduce((sum, fileGroup) => sum + Math.max(0, Number(fileGroup.removedLines) || 0), 0);
8877
+
8638
8878
  return {
8639
8879
  kind: "diff_thread",
8640
8880
  token: group.token,
8641
8881
  threadId: group.threadId,
8642
8882
  threadLabel: group.threadLabel,
8883
+ sourceMode: cleanText(group.sourceMode || ""),
8643
8884
  changedFileCount: files.length,
8644
- latestChangedAtMs: group.latestChangedAtMs,
8645
- latestChangeType: group.latestChangeType,
8646
- latestChangeFileRefs: normalizeTimelineFileRefs(group.latestChangeFileRefs),
8647
- diffAddedLines: group.diffAddedLines,
8648
- diffRemovedLines: group.diffRemovedLines,
8885
+ latestChangedAtMs,
8886
+ latestChangeType: latestChangeTypes.length === 1 ? latestChangeTypes[0] : "",
8887
+ latestChangeFileRefs,
8888
+ diffAddedLines,
8889
+ diffRemovedLines,
8649
8890
  files,
8650
8891
  };
8651
8892
  })
8893
+ .filter((group) => !shouldExcludeCodeThread(runtime, group.threadId))
8652
8894
  .filter((group) => group.changedFileCount > 0)
8653
8895
  .sort((left, right) => Number(right.latestChangedAtMs ?? 0) - Number(left.latestChangedAtMs ?? 0));
8654
8896
  }
@@ -9165,18 +9407,20 @@ function buildTimelineFileEventDetail(entry, locale) {
9165
9407
  }
9166
9408
 
9167
9409
  function buildDiffThreadDetail(group, locale) {
9410
+ const changedFileCount = Math.max(0, Number(group.changedFileCount) || 0);
9168
9411
  return {
9169
9412
  kind: "diff_thread",
9170
9413
  token: group.token,
9171
9414
  threadId: cleanText(group.threadId || ""),
9172
9415
  title: cleanText(group.threadLabel || "") || kindTitle(locale, "diff_thread"),
9173
9416
  threadLabel: group.threadLabel || "",
9417
+ sourceMode: cleanText(group.sourceMode || ""),
9174
9418
  createdAtMs: Number(group.latestChangedAtMs) || 0,
9175
- changedFileCount: Math.max(0, Number(group.changedFileCount) || 0),
9419
+ changedFileCount,
9176
9420
  diffAddedLines: Math.max(0, Number(group.diffAddedLines) || 0),
9177
9421
  diffRemovedLines: Math.max(0, Number(group.diffRemovedLines) || 0),
9178
9422
  messageHtml: renderMessageHtml(
9179
- t(locale, "detail.diffThread.copy", { count: Math.max(0, Number(group.changedFileCount) || 0) }),
9423
+ diffThreadDetailCopy(locale, group.sourceMode, changedFileCount),
9180
9424
  `<p>${escapeHtml(t(locale, "detail.detailUnavailable"))}</p>`
9181
9425
  ),
9182
9426
  files: Array.isArray(group.files)
package/web/i18n.js CHANGED
@@ -65,12 +65,12 @@ const translations = {
65
65
  "tab.timeline.description": "Conversation updates and operational events across recent threads.",
66
66
  "tab.diff.title": "Diff",
67
67
  "tab.diff.label": "Diff",
68
- "tab.diff.eyebrow": "Current changes",
69
- "tab.diff.description": "Review the current unstaged file changes from Codex in a mobile-friendly diff view.",
68
+ "tab.diff.eyebrow": "Code changes",
69
+ "tab.diff.description": "Review file changes from Codex in a mobile-friendly diff view.",
70
70
  "tab.code.title": "Code",
71
71
  "tab.code.label": "Code",
72
- "tab.code.eyebrow": "Current changes",
73
- "tab.code.description": "Review the current unstaged file changes from Codex in a mobile-friendly diff view on your device.",
72
+ "tab.code.eyebrow": "Code changes",
73
+ "tab.code.description": "Review file changes from Codex in a mobile-friendly diff view on your device.",
74
74
  "tab.completed.title": "Completed",
75
75
  "tab.completed.label": "Completed",
76
76
  "tab.completed.eyebrow": "History",
@@ -112,7 +112,7 @@ const translations = {
112
112
  "empty.pending":
113
113
  "Nothing needs attention right now. New approvals, plans, and choices will appear here first.",
114
114
  "empty.timeline": "No timeline activity is available yet. New conversation updates will show up here.",
115
- "empty.diff": "No current unstaged file changes are available.",
115
+ "empty.diff": "No file changes are available right now.",
116
116
  "empty.completed": "Completed history is empty for now.",
117
117
  "empty.default": "Nothing to show right now.",
118
118
  "detail.selectTitle": "Choose something from the list",
@@ -127,7 +127,11 @@ const translations = {
127
127
  "detail.diffTitle": "Diff",
128
128
  "detail.diffUnavailable": "Diff is not available for this change. Showing touched files only.",
129
129
  "detail.diffThread.copy": ({ count }) =>
130
+ `This thread has ${count} ${count === 1 ? "changed file" : "changed files"}. Review each file below.`,
131
+ "detail.diffThread.copy.current": ({ count }) =>
130
132
  `This thread currently has ${count} ${count === 1 ? "unstaged changed file" : "unstaged changed files"}. Review each file below.`,
133
+ "detail.diffThread.copy.latest": ({ count }) =>
134
+ `This thread has ${count} ${count === 1 ? "latest observed file change" : "latest observed file changes"}. Review each file below.`,
131
135
  "detail.fileEvent.read": "Codex checked these files in this step.",
132
136
  "detail.fileEvent.write": "Codex updated these files in this step.",
133
137
  "detail.fileEvent.create": "Codex created these files in this step.",
@@ -182,14 +186,14 @@ const translations = {
182
186
  "summary.plan": "Check the proposed plan before Codex starts implementing it.",
183
187
  "summary.choice": "Pick the option Codex needs to continue.",
184
188
  "summary.fileEvent": "Review the files Codex checked or updated in this step.",
185
- "summary.diffThread": "Review the current unstaged file changes for this thread.",
189
+ "summary.diffThread": "Review the file changes for this thread.",
186
190
  "summary.default": "Open the item to review its details.",
187
191
  "intent.approval": "Approve or reject this request.",
188
192
  "intent.plan": "Review this plan before Codex continues.",
189
193
  "intent.choice": "Choose an answer so Codex can continue.",
190
194
  "intent.completed": "Finished. Open to review the result.",
191
195
  "intent.fileEvent": "Review the files involved in this step.",
192
- "intent.diffThread": "Review the current unstaged file changes for this thread.",
196
+ "intent.diffThread": "Review the file changes for this thread.",
193
197
  "intent.userMessage": "Open this message again.",
194
198
  "intent.assistantCommentary": "Read the latest working update.",
195
199
  "intent.assistantFinal": "Read Codex's final reply for this turn.",
@@ -224,7 +228,7 @@ const translations = {
224
228
  "fileEvent.timeline.rename": ({ count }) => `Renamed ${count} ${count === 1 ? "file" : "files"}`,
225
229
  "diff.threadSummary": ({ count }) => `Changed ${count} ${count === 1 ? "file" : "files"}`,
226
230
  "diff.latestChange": "Latest change",
227
- "diff.latestChangeFallback": "Current unstaged changes are available in this thread.",
231
+ "diff.latestChangeFallback": "File changes are available in this thread.",
228
232
  "settings.intro": "Check pairing, language, install status, and Web Push health for this device.",
229
233
  "settings.section.overview": "Quick setup",
230
234
  "settings.section.notifications": "Notifications",
@@ -588,12 +592,12 @@ const translations = {
588
592
  "tab.timeline.description": "最近のスレッドにまたがる会話の更新と操作イベントです。",
589
593
  "tab.diff.title": "差分",
590
594
  "tab.diff.label": "差分",
591
- "tab.diff.eyebrow": "現在の変更",
592
- "tab.diff.description": "Codex が残している現在の未ステージ変更を端末で確認します。",
595
+ "tab.diff.eyebrow": "コード変更",
596
+ "tab.diff.description": "Codex のファイル変更を端末で確認します。",
593
597
  "tab.code.title": "コード",
594
598
  "tab.code.label": "コード",
595
- "tab.code.eyebrow": "現在の変更",
596
- "tab.code.description": "Codex が残している現在の未ステージ変更を端末で確認します。",
599
+ "tab.code.eyebrow": "コード変更",
600
+ "tab.code.description": "Codex のファイル変更を端末で確認します。",
597
601
  "tab.completed.title": "完了",
598
602
  "tab.completed.label": "完了",
599
603
  "tab.completed.eyebrow": "履歴",
@@ -634,7 +638,7 @@ const translations = {
634
638
  "shell.subtitle.detail": "いまの流れを途切れさせずに、項目を確認して操作できます。",
635
639
  "empty.pending": "いま対応が必要な項目はありません。新しい承認、プラン、選択がここに表示されます。",
636
640
  "empty.timeline": "まだタイムライン項目はありません。新しい会話の更新がここに表示されます。",
637
- "empty.diff": "いま確認できる未ステージの変更はありません。",
641
+ "empty.diff": "いま確認できるファイル変更はありません。",
638
642
  "empty.completed": "完了履歴はまだありません。",
639
643
  "empty.default": "いま表示できる項目はありません。",
640
644
  "detail.selectTitle": "一覧から項目を選んでください",
@@ -648,7 +652,9 @@ const translations = {
648
652
  "detail.filesTitle": "関連ファイル",
649
653
  "detail.diffTitle": "差分",
650
654
  "detail.diffUnavailable": "この変更の差分はまだ利用できません。対象ファイルのみ表示します。",
651
- "detail.diffThread.copy": ({ count }) => `このスレッドで現在未ステージの変更があるファイルは ${count}件です。下でファイルごとに確認できます。`,
655
+ "detail.diffThread.copy": ({ count }) => `このスレッドで変更があるファイルは ${count}件です。下でファイルごとに確認できます。`,
656
+ "detail.diffThread.copy.current": ({ count }) => `このスレッドで現在未ステージの変更があるファイルは ${count}件です。下でファイルごとに確認できます。`,
657
+ "detail.diffThread.copy.latest": ({ count }) => `このスレッドで最後に観測した変更があるファイルは ${count}件です。下でファイルごとに確認できます。`,
652
658
  "detail.fileEvent.read": "このステップで確認したファイルです。",
653
659
  "detail.fileEvent.write": "このステップで更新したファイルです。",
654
660
  "detail.fileEvent.create": "このステップで作成したファイルです。",
@@ -703,14 +709,14 @@ const translations = {
703
709
  "summary.plan": "Codex が実装を始める前に、提案されたプランを確認します。",
704
710
  "summary.choice": "Codex が先へ進むために必要な選択肢を選びます。",
705
711
  "summary.fileEvent": "このステップで触れたファイルを確認します。",
706
- "summary.diffThread": "このスレッドで現在未ステージの変更が残っているファイルを確認します。",
712
+ "summary.diffThread": "このスレッドのファイル変更を確認します。",
707
713
  "summary.default": "項目を開いて詳細を確認します。",
708
714
  "intent.approval": "このリクエストを承認するか拒否します。",
709
715
  "intent.plan": "Codex が続行する前にこのプランを確認します。",
710
716
  "intent.choice": "Codex が続行できるように回答を選びます。",
711
717
  "intent.completed": "完了済みです。結果を確認できます。",
712
718
  "intent.fileEvent": "このステップで触れたファイルを確認します。",
713
- "intent.diffThread": "このスレッドで現在未ステージの変更が残っているファイルを確認します。",
719
+ "intent.diffThread": "このスレッドのファイル変更を確認します。",
714
720
  "intent.userMessage": "このメッセージを開き直します。",
715
721
  "intent.assistantCommentary": "最新の途中経過を確認します。",
716
722
  "intent.assistantFinal": "このターンの Codex の最終回答を確認します。",
@@ -745,7 +751,7 @@ const translations = {
745
751
  "fileEvent.timeline.rename": ({ count }) => `ファイル名を変更 ${count}件`,
746
752
  "diff.threadSummary": ({ count }) => `変更ファイル ${count}件`,
747
753
  "diff.latestChange": "最終変更",
748
- "diff.latestChangeFallback": "このスレッドに現在未ステージの変更があります。",
754
+ "diff.latestChangeFallback": "このスレッドにファイル変更があります。",
749
755
  "settings.intro": "この端末のペアリング、言語、インストール状況、Web Push の状態を確認できます。",
750
756
  "settings.section.overview": "クイック確認",
751
757
  "settings.section.notifications": "通知",