authorgravatar for julius0216@outlook.comJulius Marminge <julius0216@outlook.com> 2026-02-19 00:11:02-08:00
committergravatar for jmarminge@gmail.comJulius Marminge <jmarminge@gmail.com> 2026-02-19 00:11:40-08:00
logeecb53498ad182f51ad1743280e8d22e5e1d116c
tree0fa7a3d09e120043257b60deb27d9e99f8d9788e
parent5cf140b0ee2d7320d101b082217a90afb7f230ce

Mark active thread visited on selection

- 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 time

3 files changed, 39 insertions(+), 0 deletions(-)

apps/web/src/routes/__root.tsx+9
......@@ -67,6 +67,15 @@ function EventRouter() {
6767 });
6868 }, [activeThreadId, api, dispatch, queryClient]);
6969
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
7079 useEffect(() => {
7180 if (!api) return;
7281 return api.terminal.onEvent((event) => {
apps/web/src/store.test.ts+18
......@@ -815,6 +815,24 @@ describe("store reducer thread continuity", () => {
815815 expect(next.threads[0]?.lastVisitedAt).toBe(completedAt);
816816 });
817817
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
818836 it("reverts thread state to a checkpoint snapshot", () => {
819837 const state = makeState(
820838 makeThread({
apps/web/src/store.ts+12
......@@ -105,6 +105,7 @@ type Action =
105105 branch: string | null;
106106 worktreePath: string | null;
107107 }
108 | { type: "MARK_THREAD_VISITED"; threadId: string; visitedAt?: string }
108109 | { type: "SET_RUNTIME_MODE"; mode: RuntimeMode }
109110 | { type: "DELETE_THREAD"; threadId: string };
110111
......@@ -1141,6 +1142,17 @@ export function reducer(state: AppState, action: Action): AppState {
11411142 };
11421143 }
11431144
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
11441156 case "SET_RUNTIME_MODE":
11451157 return {
11461158 ...state,