diff --git a/apps/web/src/components/ChatView.tsx b/apps/web/src/components/ChatView.tsx index 765a2d5f9ae..0f22ba66be3 100644 --- a/apps/web/src/components/ChatView.tsx +++ b/apps/web/src/components/ChatView.tsx @@ -1021,7 +1021,10 @@ export default function ChatView(props: ChatViewProps) { const lastVisitedAt = activeThreadLastVisitedAt ? Date.parse(activeThreadLastVisitedAt) : NaN; if (!Number.isNaN(lastVisitedAt) && lastVisitedAt >= turnCompletedAt) return; - markThreadVisited(scopedThreadKey(scopeThreadRef(serverThread.environmentId, serverThread.id))); + markThreadVisited( + scopedThreadKey(scopeThreadRef(serverThread.environmentId, serverThread.id)), + activeLatestTurn.completedAt, + ); }, [ activeLatestTurn?.completedAt, activeThreadLastVisitedAt, diff --git a/apps/web/src/uiStateStore.test.ts b/apps/web/src/uiStateStore.test.ts index f63208d9ba7..ca5ed2b4b18 100644 --- a/apps/web/src/uiStateStore.test.ts +++ b/apps/web/src/uiStateStore.test.ts @@ -4,6 +4,7 @@ import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; import { clearThreadUi, hydratePersistedProjectState, + markThreadVisited, markThreadUnread, PERSISTED_STATE_KEY, type PersistedUiState, @@ -27,6 +28,28 @@ function makeUiState(overrides: Partial = {}): UiState { } describe("uiStateStore pure functions", () => { + it("markThreadVisited stores the provided server timestamp", () => { + const threadId = ThreadId.make("thread-1"); + const initialState = makeUiState(); + + const next = markThreadVisited(initialState, threadId, "2026-02-25T12:30:00.700Z"); + + expect(next.threadLastVisitedAtById[threadId]).toBe("2026-02-25T12:30:00.700Z"); + }); + + it("markThreadVisited does not move visit state backwards under clock skew", () => { + const threadId = ThreadId.make("thread-1"); + const initialState = makeUiState({ + threadLastVisitedAtById: { + [threadId]: "2026-02-25T12:30:00.700Z", + }, + }); + + const next = markThreadVisited(initialState, threadId, "2026-02-25T12:30:00.000Z"); + + expect(next).toBe(initialState); + }); + it("markThreadUnread moves lastVisitedAt before completion for a completed thread", () => { const threadId = ThreadId.make("thread-1"); const latestTurnCompletedAt = "2026-02-25T12:30:00.000Z";