diff --git a/src/lib/boss-projections.ts b/src/lib/boss-projections.ts index c90b1d4..2533063 100644 --- a/src/lib/boss-projections.ts +++ b/src/lib/boss-projections.ts @@ -355,6 +355,7 @@ function buildConversationItem(state: BossState, project: Project): Conversation const folderLabel = project.threadMeta?.folderName ?? ""; const activityIconCount = deriveConversationActivityIconCount(state, project); const topPinnedLabel = isTopPinnedConversation(project) ? "置顶" : undefined; + const latestConversationActivityAt = deriveLatestConversationActivityAt(project); const groupMembers = project.isGroup ? project.groupMembers.map((member) => ({ threadId: member.threadId, @@ -379,8 +380,8 @@ function buildConversationItem(state: BossState, project: Project): Conversation activityIconCount, topPinnedLabel, manualPinned: Boolean(project.pinned && !project.systemPinned), - latestReplyAt: project.lastMessageAt, - latestReplyLabel: formatTimestampLabel(project.lastMessageAt), + latestReplyAt: latestConversationActivityAt, + latestReplyLabel: formatTimestampLabel(latestConversationActivityAt), unreadCount: project.unreadCount, riskLevel: project.riskLevel, activeDeviceCount: devices.length, @@ -403,6 +404,31 @@ function buildConversationItem(state: BossState, project: Project): Conversation } satisfies ConversationItem; } +function deriveLatestConversationActivityAt(project: Project) { + const candidates = [ + project.lastMessageAt, + project.threadMeta?.lastObservedCodexActivityAt, + project.projectUnderstanding?.updatedAt, + project.updatedAt, + ].filter(Boolean) as string[]; + + let latest = candidates[0]; + let latestTs = latest ? Date.parse(latest) : Number.NEGATIVE_INFINITY; + + for (const candidate of candidates.slice(1)) { + const candidateTs = Date.parse(candidate); + if (!Number.isFinite(candidateTs)) { + continue; + } + if (!Number.isFinite(latestTs) || candidateTs > latestTs) { + latest = candidate; + latestTs = candidateTs; + } + } + + return latest ?? project.lastMessageAt; +} + function deriveConversationActivityIconCount(state: BossState, project: Project): number { let count = 0; diff --git a/tests/conversation-home-items.test.ts b/tests/conversation-home-items.test.ts index 82db352..3865ea7 100644 --- a/tests/conversation-home-items.test.ts +++ b/tests/conversation-home-items.test.ts @@ -8,6 +8,7 @@ let runtimeRoot = ""; let readState: (typeof import("../src/lib/boss-data"))["readState"]; let getConversationHomeItems: (typeof import("../src/lib/boss-projections"))["getConversationHomeItems"]; let getConversationFolderView: (typeof import("../src/lib/boss-projections"))["getConversationFolderView"]; +let formatTimestampLabel: (typeof import("../src/lib/boss-projections"))["formatTimestampLabel"]; async function setup() { if (runtimeRoot) return; @@ -22,6 +23,7 @@ async function setup() { readState = data.readState; getConversationHomeItems = projections.getConversationHomeItems; getConversationFolderView = projections.getConversationFolderView; + formatTimestampLabel = projections.formatTimestampLabel; } test.after(async () => { @@ -199,3 +201,35 @@ test("conversation items keep a safe context ring even when no thread snapshot e assert.equal(directThread?.contextBudgetIndicator.percent, 100); assert.equal(directThread?.contextBudgetIndicator.level, "safe"); }); + +test("conversation items prefer latest observed codex activity over stale last message time", async () => { + await setup(); + const state = await readState(); + const baseProject = buildImportedThreadProject( + "mac-studio", + "stale-thread", + "Talking", + "talking", + "树莓派二代查询", + "thread-stale", + "2026-04-04T06:12:00+08:00", + ); + + state.projects = state.projects.filter((project) => project.id === "master-agent"); + state.projects.push( + { + ...baseProject, + threadMeta: { + ...baseProject.threadMeta, + lastObservedCodexActivityAt: "2026-04-04T11:48:00+08:00", + }, + }, + ); + + const items = getConversationHomeItems(state); + const thread = items.find((item) => item.projectId === "stale-thread"); + + assert.ok(thread); + assert.equal(thread?.latestReplyAt, "2026-04-04T11:48:00+08:00"); + assert.equal(thread?.latestReplyLabel, formatTimestampLabel("2026-04-04T11:48:00+08:00")); +});