diff --git a/apps/web/src/routes/__root.tsx b/apps/web/src/routes/__root.tsx index 33b578674be2d612cc2b83a1a60de1a7136c770e..57c6085349c3baaee549952af44f85a70ff233c6 100644 --- a/apps/web/src/routes/__root.tsx +++ b/apps/web/src/routes/__root.tsx @@ -67,6 +67,15 @@ function EventRouter() { }); }, [activeThreadId, api, dispatch, queryClient]); + useEffect(() => { + if (!activeThreadId) return; + dispatch({ + type: "MARK_THREAD_VISITED", + threadId: activeThreadId, + visitedAt: new Date().toISOString(), + }); + }, [activeThreadId, dispatch]); + useEffect(() => { if (!api) return; return api.terminal.onEvent((event) => { diff --git a/apps/web/src/store.test.ts b/apps/web/src/store.test.ts index f7c31a90f30e3f3ffd3d23ef8f2a401fd12d7bb5..b1095463d5ded3800c22f3771f8e9aa1ed954d3c 100644 --- a/apps/web/src/store.test.ts +++ b/apps/web/src/store.test.ts @@ -815,6 +815,24 @@ describe("store reducer thread continuity", () => { expect(next.threads[0]?.lastVisitedAt).toBe(completedAt); }); + it("marks a thread as visited when selected", () => { + const state = makeState( + makeThread({ + latestTurnCompletedAt: "2026-02-08T10:00:10.000Z", + lastVisitedAt: "2026-02-08T10:00:00.000Z", + }), + ); + + const nextVisitedAt = "2026-02-08T10:00:20.000Z"; + const next = reducer(state, { + type: "MARK_THREAD_VISITED", + threadId: "thread-local-1", + visitedAt: nextVisitedAt, + }); + + expect(next.threads[0]?.lastVisitedAt).toBe(nextVisitedAt); + }); + it("reverts thread state to a checkpoint snapshot", () => { const state = makeState( makeThread({ diff --git a/apps/web/src/store.ts b/apps/web/src/store.ts index e701522470c6cd477faff899894abb651310bf5d..265f58abbae4911c0f19f45139e2106462f0613e 100644 --- a/apps/web/src/store.ts +++ b/apps/web/src/store.ts @@ -105,6 +105,7 @@ type Action = branch: string | null; worktreePath: string | null; } + | { type: "MARK_THREAD_VISITED"; threadId: string; visitedAt?: string } | { type: "SET_RUNTIME_MODE"; mode: RuntimeMode } | { type: "DELETE_THREAD"; threadId: string }; @@ -1141,6 +1142,17 @@ export function reducer(state: AppState, action: Action): AppState { }; } + case "MARK_THREAD_VISITED": { + const visitedAt = action.visitedAt ?? new Date().toISOString(); + return { + ...state, + threads: updateThread(state.threads, action.threadId, (t) => ({ + ...t, + lastVisitedAt: visitedAt, + })), + }; + } + case "SET_RUNTIME_MODE": return { ...state,