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() {...@@ -67,6 +67,15 @@ function EventRouter() {
67 });67 });
68 }, [activeThreadId, api, dispatch, queryClient]);68 }, [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
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 });
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
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 };
110111
...@@ -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 }
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
1144 case "SET_RUNTIME_MODE":1156 case "SET_RUNTIME_MODE":
1145 return {1157 return {
1146 ...state,1158 ...state,