| author | |
| committer | |
| log | eecb53498ad182f51ad1743280e8d22e5e1d116c |
| tree | 0fa7a3d09e120043257b60deb27d9e99f8d9788e |
| parent | 5cf140b0ee2d7320d101b082217a90afb7f230ce |
- Dispatch `MARK_THREAD_VISITED` when `activeThreadId` changes in root route
- Add reducer handling to update a thread's `lastVisitedAt`
- Add reducer test coverage for selecting a thread updates visit time3 files changed, 39 insertions(+), 0 deletions(-)
apps/web/src/routes/__root.tsx+9| ... | @@ -67,6 +67,15 @@ function EventRouter() { | ... | @@ -67,6 +67,15 @@ function EventRouter() { |
| 67 | }); | 67 | }); |
| 68 | }, [activeThreadId, api, dispatch, queryClient]); | 68 | }, [activeThreadId, api, dispatch, queryClient]); |
| 69 | 69 | ||
| 70 | useEffect(() => { | ||
| 71 | if (!activeThreadId) return; | ||
| 72 | dispatch({ | ||
| 73 | type: "MARK_THREAD_VISITED", | ||
| 74 | threadId: activeThreadId, | ||
| 75 | visitedAt: new Date().toISOString(), | ||
| 76 | }); | ||
| 77 | }, [activeThreadId, dispatch]); | ||
| 78 | |||
| 70 | useEffect(() => { | 79 | useEffect(() => { |
| 71 | if (!api) return; | 80 | if (!api) return; |
| 72 | return api.terminal.onEvent((event) => { | 81 | return api.terminal.onEvent((event) => { |
apps/web/src/store.test.ts+18| ... | @@ -815,6 +815,24 @@ describe("store reducer thread continuity", () => { | ... | @@ -815,6 +815,24 @@ describe("store reducer thread continuity", () => { |
| 815 | expect(next.threads[0]?.lastVisitedAt).toBe(completedAt); | 815 | expect(next.threads[0]?.lastVisitedAt).toBe(completedAt); |
| 816 | }); | 816 | }); |
| 817 | 817 | ||
| 818 | it("marks a thread as visited when selected", () => { | ||
| 819 | const state = makeState( | ||
| 820 | makeThread({ | ||
| 821 | latestTurnCompletedAt: "2026-02-08T10:00:10.000Z", | ||
| 822 | lastVisitedAt: "2026-02-08T10:00:00.000Z", | ||
| 823 | }), | ||
| 824 | ); | ||
| 825 | |||
| 826 | const nextVisitedAt = "2026-02-08T10:00:20.000Z"; | ||
| 827 | const next = reducer(state, { | ||
| 828 | type: "MARK_THREAD_VISITED", | ||
| 829 | threadId: "thread-local-1", | ||
| 830 | visitedAt: nextVisitedAt, | ||
| 831 | }); | ||
| 832 | |||
| 833 | expect(next.threads[0]?.lastVisitedAt).toBe(nextVisitedAt); | ||
| 834 | }); | ||
| 835 | |||
| 818 | it("reverts thread state to a checkpoint snapshot", () => { | 836 | it("reverts thread state to a checkpoint snapshot", () => { |
| 819 | const state = makeState( | 837 | const state = makeState( |
| 820 | makeThread({ | 838 | makeThread({ |
apps/web/src/store.ts+12| ... | @@ -105,6 +105,7 @@ type Action = | ... | @@ -105,6 +105,7 @@ type Action = |
| 105 | branch: string | null; | 105 | branch: string | null; |
| 106 | worktreePath: string | null; | 106 | worktreePath: string | null; |
| 107 | } | 107 | } |
| 108 | | { type: "MARK_THREAD_VISITED"; threadId: string; visitedAt?: string } | ||
| 108 | | { type: "SET_RUNTIME_MODE"; mode: RuntimeMode } | 109 | | { type: "SET_RUNTIME_MODE"; mode: RuntimeMode } |
| 109 | | { type: "DELETE_THREAD"; threadId: string }; | 110 | | { type: "DELETE_THREAD"; threadId: string }; |
| 110 | 111 | ||
| ... | @@ -1141,6 +1142,17 @@ export function reducer(state: AppState, action: Action): AppState { | ... | @@ -1141,6 +1142,17 @@ export function reducer(state: AppState, action: Action): AppState { |
| 1141 | }; | 1142 | }; |
| 1142 | } | 1143 | } |
| 1143 | 1144 | ||
| 1145 | case "MARK_THREAD_VISITED": { | ||
| 1146 | const visitedAt = action.visitedAt ?? new Date().toISOString(); | ||
| 1147 | return { | ||
| 1148 | ...state, | ||
| 1149 | threads: updateThread(state.threads, action.threadId, (t) => ({ | ||
| 1150 | ...t, | ||
| 1151 | lastVisitedAt: visitedAt, | ||
| 1152 | })), | ||
| 1153 | }; | ||
| 1154 | } | ||
| 1155 | |||
| 1144 | case "SET_RUNTIME_MODE": | 1156 | case "SET_RUNTIME_MODE": |
| 1145 | return { | 1157 | return { |
| 1146 | ...state, | 1158 | ...state, |