viveworker 0.1.9 → 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.
@@ -85,8 +85,16 @@ const state = await loadState(config.stateFile);
85
85
  const migratedPairedDevicesStateChanged = migratePairedDevicesState({ config, state });
86
86
  const restoredPendingPlanStateChanged = restorePendingPlanRequests({ config, runtime, state });
87
87
  const restoredPendingUserInputStateChanged = restorePendingUserInputRequests({ config, runtime, state });
88
- runtime.recentHistoryItems = normalizeHistoryItems(state.recentHistoryItems ?? [], config.maxHistoryItems);
89
- runtime.recentTimelineEntries = normalizeTimelineEntries(state.recentTimelineEntries ?? [], config.maxTimelineEntries);
88
+ const initialHistoryItems = normalizeHistoryItems(state.recentHistoryItems ?? [], config.maxHistoryItems);
89
+ const initialTimelineEntries = normalizeTimelineEntries(state.recentTimelineEntries ?? [], config.maxTimelineEntries);
90
+ const normalizedHistoryStateChanged =
91
+ JSON.stringify(initialHistoryItems) !== JSON.stringify(Array.isArray(state.recentHistoryItems) ? state.recentHistoryItems : []);
92
+ const normalizedTimelineStateChanged =
93
+ JSON.stringify(initialTimelineEntries) !== JSON.stringify(Array.isArray(state.recentTimelineEntries) ? state.recentTimelineEntries : []);
94
+ runtime.recentHistoryItems = initialHistoryItems;
95
+ runtime.recentTimelineEntries = initialTimelineEntries;
96
+ state.recentHistoryItems = initialHistoryItems;
97
+ state.recentTimelineEntries = initialTimelineEntries;
90
98
  const migratedRecentCodeEventsStateChanged = migrateRecentCodeEventsState({ config, runtime, state });
91
99
  const restoredTimelineImagePathsStateChanged = await backfillPersistedTimelineImagePaths({ config, runtime, state });
92
100
  runtime.historyFileState.offset = Number(state.historyFileOffset) || 0;
@@ -320,6 +328,17 @@ function fileEventDetailCopy(locale, fileEventType) {
320
328
  }
321
329
  }
322
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
+
323
342
  function inferTimelineOutcome(kind, summary = "", messageText = "") {
324
343
  const normalizedKind = cleanText(kind || "");
325
344
  if (!normalizedKind) {
@@ -1756,6 +1775,205 @@ function resolveCurrentChangeOwner(change, candidates) {
1756
1775
  return null;
1757
1776
  }
1758
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
+
1759
1977
  function handleSignal() {
1760
1978
  runtime.stopping = true;
1761
1979
  }
@@ -1791,7 +2009,11 @@ function normalizeHistoryItem(raw) {
1791
2009
 
1792
2010
  const stableId = cleanText(raw.stableId ?? raw.id ?? "");
1793
2011
  const kind = cleanText(raw.kind ?? "");
1794
- const title = cleanText(raw.title ?? "");
2012
+ const threadId = cleanText(raw.threadId ?? extractConversationIdFromStableId(stableId) ?? "");
2013
+ const rawThreadLabel = cleanText(raw.threadLabel ?? "");
2014
+ const threadLabel = preferTitleOnlyJsonThreadLabel(rawThreadLabel, threadId, raw.messageText, raw.summary, raw.detailText, raw.message);
2015
+ const title =
2016
+ cleanText(raw.title ?? "") || (threadLabel ? formatTitle(kindTitle(DEFAULT_LOCALE, kind), threadLabel) : kindTitle(DEFAULT_LOCALE, kind));
1795
2017
  const messageText = normalizeTimelineMessageText(raw.messageText ?? "");
1796
2018
  const summary = normalizeNotificationText(raw.summary ?? "") || formatNotificationBody(messageText, 100) || "";
1797
2019
  const createdAtMs = Number(raw.createdAtMs) || Date.now();
@@ -1805,9 +2027,9 @@ function normalizeHistoryItem(raw) {
1805
2027
  stableId,
1806
2028
  token: cleanText(raw.token ?? "") || historyToken(stableId),
1807
2029
  kind,
1808
- threadId: cleanText(raw.threadId ?? extractConversationIdFromStableId(stableId) ?? ""),
2030
+ threadId,
1809
2031
  title,
1810
- threadLabel: cleanText(raw.threadLabel ?? ""),
2032
+ threadLabel,
1811
2033
  summary,
1812
2034
  messageText,
1813
2035
  imagePaths: normalizeTimelineImagePaths(raw.imagePaths ?? raw.localImagePaths ?? []),
@@ -1922,6 +2144,7 @@ function normalizeTimelineEntry(raw) {
1922
2144
  return null;
1923
2145
  }
1924
2146
 
2147
+ const threadId = cleanText(raw.threadId ?? extractConversationIdFromStableId(stableId) ?? "");
1925
2148
  const messageText = normalizeTimelineMessageText(raw.messageText ?? "");
1926
2149
  const fileEventType = normalizeTimelineFileEventType(raw.fileEventType ?? "");
1927
2150
  const diffText = normalizeTimelineDiffText(raw.diffText ?? "");
@@ -1932,7 +2155,14 @@ function normalizeTimelineEntry(raw) {
1932
2155
  formatNotificationBody(messageText, 180) ||
1933
2156
  (kind === "file_event" ? "" : cleanText(raw.title ?? "")) ||
1934
2157
  "";
1935
- const threadLabel = cleanText(raw.threadLabel ?? "");
2158
+ const threadLabel = preferTitleOnlyJsonThreadLabel(
2159
+ cleanText(raw.threadLabel ?? ""),
2160
+ threadId,
2161
+ raw.messageText,
2162
+ raw.summary,
2163
+ raw.detailText,
2164
+ raw.message
2165
+ );
1936
2166
  const title =
1937
2167
  cleanText(raw.title ?? "") ||
1938
2168
  (kind === "file_event" ? fileEventTitle(DEFAULT_LOCALE, fileEventType) : "") ||
@@ -1944,7 +2174,7 @@ function normalizeTimelineEntry(raw) {
1944
2174
  stableId,
1945
2175
  token: cleanText(raw.token ?? "") || historyToken(stableId),
1946
2176
  kind,
1947
- threadId: cleanText(raw.threadId ?? extractConversationIdFromStableId(stableId) ?? ""),
2177
+ threadId,
1948
2178
  threadLabel,
1949
2179
  title,
1950
2180
  summary,
@@ -6752,6 +6982,90 @@ function getNativeThreadLabel({ runtime, conversationId, cwd }) {
6752
6982
  return shortId(normalizedConversationId) || "Codex task";
6753
6983
  }
6754
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
+
6755
7069
  async function findRolloutThreadCwd(runtime, conversationId) {
6756
7070
  const normalizedConversationId = cleanText(conversationId || "");
6757
7071
  if (!normalizedConversationId) {
@@ -8427,6 +8741,7 @@ async function buildDiffInboxItems(runtime, state, config, locale) {
8427
8741
  token: group.token,
8428
8742
  threadId: group.threadId,
8429
8743
  threadLabel: group.threadLabel || "",
8744
+ sourceMode: cleanText(group.sourceMode || ""),
8430
8745
  title: cleanText(group.threadLabel || "") || kindTitle(locale, "diff_thread"),
8431
8746
  summary: t(locale, "diff.threadSummary", { count: group.changedFileCount }),
8432
8747
  changedFileCount: group.changedFileCount,
@@ -8467,10 +8782,12 @@ async function buildDiffThreadGroups(runtime, state, config) {
8467
8782
  .slice()
8468
8783
  .sort((left, right) => Number(right.createdAtMs ?? 0) - Number(left.createdAtMs ?? 0));
8469
8784
  const candidatesByRepoRoot = new Map();
8785
+ const nonGitRelevantItems = [];
8470
8786
 
8471
8787
  for (const item of relevantItems) {
8472
8788
  const repoRoot = await resolveCodeEventRepoRoot(item, repoRootCache);
8473
8789
  if (!repoRoot) {
8790
+ nonGitRelevantItems.push(item);
8474
8791
  continue;
8475
8792
  }
8476
8793
  const candidates = expandCodeEventOwnershipCandidates(item, repoRoot);
@@ -8492,112 +8809,42 @@ async function buildDiffThreadGroups(runtime, state, config) {
8492
8809
  continue;
8493
8810
  }
8494
8811
 
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;
8517
- }
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
+ }
8518
8823
 
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) {
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)) {
8524
8830
  continue;
8525
8831
  }
8526
-
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);
8532
- if (!fileGroup) {
8533
- fileGroup = {
8534
- fileRef: 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),
8547
- fileEventTypes: new Set(),
8548
- addedLines: 0,
8549
- removedLines: 0,
8550
- latestChangedAtMs: latestOwnerChangeAtMs,
8551
- sections: [],
8552
- };
8553
- threadGroup.filesByRef.set(fileGroupKey, fileGroup);
8554
- }
8555
-
8556
- const changeType = normalizeTimelineFileEventType(change.changeType);
8557
- if (changeType) {
8558
- fileGroup.fileEventTypes.add(changeType);
8559
- }
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) {
8589
- threadGroup.latestChangeType = "";
8590
- } else if (!threadGroup.latestChangeType && changeType && threadGroup.latestChangeFileRefs.length === 0) {
8591
- threadGroup.latestChangeType = changeType;
8592
- }
8593
- const latestFileRef = normalizedFileRef || normalizedNewFileRef || normalizedOldFileRef;
8594
- if (latestFileRef && !threadGroup.latestChangeFileRefs.includes(latestFileRef)) {
8595
- threadGroup.latestChangeFileRefs.push(latestFileRef);
8596
- }
8597
- }
8832
+ latestNonGitChangesByIdentity.set(identity, change);
8598
8833
  }
8599
8834
  }
8600
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
+
8601
8848
  return [...groupsByThread.values()]
8602
8849
  .map((group) => {
8603
8850
  const files = [...group.filesByRef.values()]
@@ -8607,7 +8854,9 @@ async function buildDiffThreadGroups(runtime, state, config) {
8607
8854
  newFileRef: fileGroup.newFileRef,
8608
8855
  fileLabel: fileGroup.fileLabel,
8609
8856
  changeType: normalizeTimelineFileEventType(fileGroup.changeType),
8610
- fileEventTypes: [...fileGroup.fileEventTypes.values()],
8857
+ fileEventTypes: Array.isArray(fileGroup.fileEventTypes)
8858
+ ? fileGroup.fileEventTypes
8859
+ : [...fileGroup.fileEventTypes.values()],
8611
8860
  addedLines: fileGroup.addedLines,
8612
8861
  removedLines: fileGroup.removedLines,
8613
8862
  latestChangedAtMs: fileGroup.latestChangedAtMs,
@@ -8615,20 +8864,33 @@ async function buildDiffThreadGroups(runtime, state, config) {
8615
8864
  }))
8616
8865
  .sort((left, right) => Number(right.latestChangedAtMs ?? 0) - Number(left.latestChangedAtMs ?? 0));
8617
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
+
8618
8878
  return {
8619
8879
  kind: "diff_thread",
8620
8880
  token: group.token,
8621
8881
  threadId: group.threadId,
8622
8882
  threadLabel: group.threadLabel,
8883
+ sourceMode: cleanText(group.sourceMode || ""),
8623
8884
  changedFileCount: files.length,
8624
- latestChangedAtMs: group.latestChangedAtMs,
8625
- latestChangeType: group.latestChangeType,
8626
- latestChangeFileRefs: normalizeTimelineFileRefs(group.latestChangeFileRefs),
8627
- diffAddedLines: group.diffAddedLines,
8628
- diffRemovedLines: group.diffRemovedLines,
8885
+ latestChangedAtMs,
8886
+ latestChangeType: latestChangeTypes.length === 1 ? latestChangeTypes[0] : "",
8887
+ latestChangeFileRefs,
8888
+ diffAddedLines,
8889
+ diffRemovedLines,
8629
8890
  files,
8630
8891
  };
8631
8892
  })
8893
+ .filter((group) => !shouldExcludeCodeThread(runtime, group.threadId))
8632
8894
  .filter((group) => group.changedFileCount > 0)
8633
8895
  .sort((left, right) => Number(right.latestChangedAtMs ?? 0) - Number(left.latestChangedAtMs ?? 0));
8634
8896
  }
@@ -9145,18 +9407,20 @@ function buildTimelineFileEventDetail(entry, locale) {
9145
9407
  }
9146
9408
 
9147
9409
  function buildDiffThreadDetail(group, locale) {
9410
+ const changedFileCount = Math.max(0, Number(group.changedFileCount) || 0);
9148
9411
  return {
9149
9412
  kind: "diff_thread",
9150
9413
  token: group.token,
9151
9414
  threadId: cleanText(group.threadId || ""),
9152
9415
  title: cleanText(group.threadLabel || "") || kindTitle(locale, "diff_thread"),
9153
9416
  threadLabel: group.threadLabel || "",
9417
+ sourceMode: cleanText(group.sourceMode || ""),
9154
9418
  createdAtMs: Number(group.latestChangedAtMs) || 0,
9155
- changedFileCount: Math.max(0, Number(group.changedFileCount) || 0),
9419
+ changedFileCount,
9156
9420
  diffAddedLines: Math.max(0, Number(group.diffAddedLines) || 0),
9157
9421
  diffRemovedLines: Math.max(0, Number(group.diffRemovedLines) || 0),
9158
9422
  messageHtml: renderMessageHtml(
9159
- t(locale, "detail.diffThread.copy", { count: Math.max(0, Number(group.changedFileCount) || 0) }),
9423
+ diffThreadDetailCopy(locale, group.sourceMode, changedFileCount),
9160
9424
  `<p>${escapeHtml(t(locale, "detail.detailUnavailable"))}</p>`
9161
9425
  ),
9162
9426
  files: Array.isArray(group.files)
@@ -12477,6 +12741,22 @@ function extractRolloutMessageText(content) {
12477
12741
  );
12478
12742
  }
12479
12743
 
12744
+ function extractTitleOnlyJsonTitleFromRolloutContent(content) {
12745
+ if (!Array.isArray(content)) {
12746
+ return "";
12747
+ }
12748
+ for (const entry of content) {
12749
+ if (!isPlainObject(entry) || (entry.type !== "input_text" && entry.type !== "output_text")) {
12750
+ continue;
12751
+ }
12752
+ const title = extractTitleOnlyJsonTitle(entry.text ?? "");
12753
+ if (title) {
12754
+ return title;
12755
+ }
12756
+ }
12757
+ return "";
12758
+ }
12759
+
12480
12760
  function rolloutContentHasImages(content) {
12481
12761
  if (!Array.isArray(content)) {
12482
12762
  return false;
@@ -12592,6 +12872,22 @@ async function extractRolloutThreadMetadata(filePath) {
12592
12872
  break;
12593
12873
  }
12594
12874
  }
12875
+ } else if (!metadata.titleCandidate && entry.payload?.type === "message" && entry.payload?.role === "assistant") {
12876
+ const titleCandidate = truncate(cleanText(extractTitleOnlyJsonTitleFromRolloutContent(entry.payload.content)), 90);
12877
+ if (titleCandidate) {
12878
+ metadata.titleCandidate = titleCandidate;
12879
+ if (metadata.threadId) {
12880
+ break;
12881
+ }
12882
+ }
12883
+ } else if (!metadata.titleCandidate && entry.type === "event_msg" && entry.payload?.type === "task_complete") {
12884
+ const titleCandidate = truncate(cleanText(extractTitleOnlyJsonTitle(entry.payload.last_agent_message ?? "")), 90);
12885
+ if (titleCandidate) {
12886
+ metadata.titleCandidate = titleCandidate;
12887
+ if (metadata.threadId) {
12888
+ break;
12889
+ }
12890
+ }
12595
12891
  }
12596
12892
  }
12597
12893
  } catch {
@@ -12768,11 +13064,12 @@ function refreshResolvedThreadLabels({ config, runtime, state }) {
12768
13064
  if (!conversationId) {
12769
13065
  return item;
12770
13066
  }
12771
- const threadLabel = getNativeThreadLabel({
13067
+ const nativeThreadLabel = getNativeThreadLabel({
12772
13068
  runtime,
12773
13069
  conversationId,
12774
13070
  cwd: "",
12775
13071
  });
13072
+ const threadLabel = preferTitleOnlyJsonThreadLabel(nativeThreadLabel, conversationId, item.messageText, item.summary);
12776
13073
  const title = formatTitle(kindTitle(config.defaultLocale, item.kind), threadLabel);
12777
13074
  if (threadLabel === item.threadLabel && title === item.title) {
12778
13075
  return item;
@@ -12802,11 +13099,12 @@ function refreshResolvedThreadLabels({ config, runtime, state }) {
12802
13099
  if (!threadId) {
12803
13100
  return entry;
12804
13101
  }
12805
- const threadLabel = getNativeThreadLabel({
13102
+ const nativeThreadLabel = getNativeThreadLabel({
12806
13103
  runtime,
12807
13104
  conversationId: threadId,
12808
13105
  cwd: "",
12809
13106
  });
13107
+ const threadLabel = preferTitleOnlyJsonThreadLabel(nativeThreadLabel, threadId, entry.messageText, entry.summary);
12810
13108
  const title = threadLabel || kindTitle(config.defaultLocale, entry.kind);
12811
13109
  if (threadLabel === entry.threadLabel && title === entry.title) {
12812
13110
  return entry;
@@ -12836,11 +13134,12 @@ function refreshResolvedThreadLabels({ config, runtime, state }) {
12836
13134
  if (!threadId) {
12837
13135
  return entry;
12838
13136
  }
12839
- const threadLabel = getNativeThreadLabel({
13137
+ const nativeThreadLabel = getNativeThreadLabel({
12840
13138
  runtime,
12841
13139
  conversationId: threadId,
12842
13140
  cwd: "",
12843
13141
  });
13142
+ const threadLabel = preferTitleOnlyJsonThreadLabel(nativeThreadLabel, threadId, entry.messageText, entry.summary);
12844
13143
  const title = threadLabel || kindTitle(config.defaultLocale, entry.kind);
12845
13144
  if (threadLabel === entry.threadLabel && title === entry.title) {
12846
13145
  return entry;
@@ -12902,6 +13201,10 @@ function summarizeNotificationText(value) {
12902
13201
  }
12903
13202
 
12904
13203
  function normalizeLongText(value) {
13204
+ const titleOnlyJsonTitle = extractTitleOnlyJsonTitle(value);
13205
+ if (titleOnlyJsonTitle) {
13206
+ return titleOnlyJsonTitle;
13207
+ }
12905
13208
  return String(stripEnvironmentContextBlocks(stripMarkdownLinks(value)) || "")
12906
13209
  .replace(/\r\n/gu, "\n")
12907
13210
  .replace(/[ \t]+\n/gu, "\n")
@@ -13013,6 +13316,53 @@ function cleanText(value) {
13013
13316
  return String(value || "").replace(/\s+/gu, " ").trim();
13014
13317
  }
13015
13318
 
13319
+ function extractTitleOnlyJsonTitle(value) {
13320
+ const rawText = String(value ?? "").trim();
13321
+ if (!rawText) {
13322
+ return "";
13323
+ }
13324
+ const parsed = safeJsonParse(rawText);
13325
+ if (!isPlainObject(parsed) || typeof parsed.title !== "string") {
13326
+ return "";
13327
+ }
13328
+ const meaningfulExtraKeys = Object.entries(parsed).filter(([key, entryValue]) => {
13329
+ if (key === "title") {
13330
+ return false;
13331
+ }
13332
+ if (entryValue == null) {
13333
+ return false;
13334
+ }
13335
+ if (typeof entryValue === "string" && cleanText(entryValue) === "") {
13336
+ return false;
13337
+ }
13338
+ if (Array.isArray(entryValue) && entryValue.length === 0) {
13339
+ return false;
13340
+ }
13341
+ if (isPlainObject(entryValue) && Object.keys(entryValue).length === 0) {
13342
+ return false;
13343
+ }
13344
+ return true;
13345
+ });
13346
+ if (meaningfulExtraKeys.length > 0) {
13347
+ return "";
13348
+ }
13349
+ return cleanText(parsed.title || "");
13350
+ }
13351
+
13352
+ function preferTitleOnlyJsonThreadLabel(rawThreadLabel, conversationId, ...candidates) {
13353
+ const preferredThreadLabel = sanitizeResolvedThreadLabel(rawThreadLabel, conversationId);
13354
+ if (preferredThreadLabel) {
13355
+ return preferredThreadLabel;
13356
+ }
13357
+ for (const candidate of candidates) {
13358
+ const titleOnlyJsonTitle = truncate(cleanText(extractTitleOnlyJsonTitle(candidate)), 90);
13359
+ if (titleOnlyJsonTitle) {
13360
+ return titleOnlyJsonTitle;
13361
+ }
13362
+ }
13363
+ return cleanText(rawThreadLabel || "");
13364
+ }
13365
+
13016
13366
  function stripMarkdownLinks(value) {
13017
13367
  return String(value || "").replace(/\[([^\]]+)\]\(([^)]+)\)/gu, "$1");
13018
13368
  }
@@ -13227,6 +13577,8 @@ async function main() {
13227
13577
  }
13228
13578
 
13229
13579
  if (
13580
+ normalizedHistoryStateChanged ||
13581
+ normalizedTimelineStateChanged ||
13230
13582
  migratedPairedDevicesStateChanged ||
13231
13583
  restoredPendingPlanStateChanged ||
13232
13584
  restoredTimelineImagePathsStateChanged ||