- Prefer route thread id over stored diff thread when rendering diffs - When visiting a different thread with diff open, retarget diff state and clear stale turn/file selection - Add reducer test coverage for open-diff retargeting behavior
Unified · Split
| @@ -107,7 +107,7 @@ export default function DiffPanel({ mode = "inline" }: DiffPanelProps) { · 106 unmodified lines | |||
| 107 | const patchViewportRef = useRef<HTMLDivElement>(null); | 107 | const patchViewportRef = useRef<HTMLDivElement>(null); |
| 108 | const params = useParams({ strict: false }); | 108 | const params = useParams({ strict: false }); |
| 109 | const routeThreadId = typeof params.threadId === "string" ? params.threadId : null; | 109 | const routeThreadId = typeof params.threadId === "string" ? params.threadId : null; |
| 110 | const activeThreadId = state.diffThreadId ?? routeThreadId; | 110 | const activeThreadId = routeThreadId ?? state.diffThreadId; |
| 111 | const activeThread = state.threads.find((thread) => thread.id === activeThreadId); | 111 | const activeThread = state.threads.find((thread) => thread.id === activeThreadId); |
| 112 | const activeThreadRuntimeId = | 112 | const activeThreadRuntimeId = |
| 113 | activeThread?.codexThreadId ?? activeThread?.session?.threadId ?? null; | 113 | activeThread?.codexThreadId ?? activeThread?.session?.threadId ?? null; |
| @@ -216,6 +216,17 @@ export default function DiffPanel({ mode = "inline" }: DiffPanelProps) { · 102 unmodified lines | |||
| 216 | target?.scrollIntoView({ block: "nearest" }); | 216 | target?.scrollIntoView({ block: "nearest" }); |
| 217 | }, [selectedFilePath, renderableFiles]); | 217 | }, [selectedFilePath, renderableFiles]); |
| 218 | 218 | ||
| 219 | useEffect(() => { | ||
| 220 | if (!state.diffOpen) return; | ||
| 221 | if (!routeThreadId) return; | ||
| 222 | if (state.diffThreadId === routeThreadId) return; | ||
| 223 | |||
| 224 | dispatch({ | ||
| 225 | type: "SET_DIFF_TARGET", | ||
| 226 | threadId: routeThreadId, | ||
| 227 | }); | ||
| 228 | }, [dispatch, routeThreadId, state.diffOpen, state.diffThreadId]); | ||
| 229 | |||
| 219 | const selectTurn = (turnId: string) => { | 230 | const selectTurn = (turnId: string) => { |
| 220 | if (!activeThread) return; | 231 | if (!activeThread) return; |
| 221 | dispatch({ | 232 | dispatch({ |
| @@ -833,6 +833,48 @@ describe("store reducer thread continuity", () => { · 832 unmodified lines | |||
| 833 | expect(next.threads[0]?.lastVisitedAt).toBe(nextVisitedAt); | 833 | expect(next.threads[0]?.lastVisitedAt).toBe(nextVisitedAt); |
| 834 | }); | 834 | }); |
| 835 | 835 | ||
| 836 | it("retargets open diff state to the visited thread", () => { | ||
| 837 | const state: AppState = { | ||
| 838 | ...makeState( | ||
| 839 | makeThread({ | ||
| 840 | id: "thread-local-1", | ||
| 841 | latestTurnCompletedAt: "2026-02-08T10:00:10.000Z", | ||
| 842 | lastVisitedAt: "2026-02-08T10:00:00.000Z", | ||
| 843 | }), | ||
| 844 | ), | ||
| 845 | threads: [ | ||
| 846 | makeThread({ | ||
| 847 | id: "thread-local-1", | ||
| 848 | latestTurnCompletedAt: "2026-02-08T10:00:10.000Z", | ||
| 849 | lastVisitedAt: "2026-02-08T10:00:00.000Z", | ||
| 850 | }), | ||
| 851 | makeThread({ | ||
| 852 | id: "thread-local-2", | ||
| 853 | session: makeSession({ sessionId: "sess-2" }), | ||
| 854 | latestTurnCompletedAt: "2026-02-08T09:00:10.000Z", | ||
| 855 | lastVisitedAt: "2026-02-08T09:00:00.000Z", | ||
| 856 | }), | ||
| 857 | ], | ||
| 858 | diffOpen: true, | ||
| 859 | diffThreadId: "thread-local-1", | ||
| 860 | diffTurnId: "turn-1", | ||
| 861 | diffFilePath: "src/old.ts", | ||
| 862 | }; | ||
| 863 | |||
| 864 | const nextVisitedAt = "2026-02-08T10:00:20.000Z"; | ||
| 865 | const next = reducer(state, { | ||
| 866 | type: "MARK_THREAD_VISITED", | ||
| 867 | threadId: "thread-local-2", | ||
| 868 | visitedAt: nextVisitedAt, | ||
| 869 | }); | ||
| 870 | |||
| 871 | expect(next.threads[1]?.lastVisitedAt).toBe(nextVisitedAt); | ||
| 872 | expect(next.diffOpen).toBe(true); | ||
| 873 | expect(next.diffThreadId).toBe("thread-local-2"); | ||
| 874 | expect(next.diffTurnId).toBeNull(); | ||
| 875 | expect(next.diffFilePath).toBeNull(); | ||
| 876 | }); | ||
| 877 | |||
| 836 | it("reverts thread state to a checkpoint snapshot", () => { | 878 | it("reverts thread state to a checkpoint snapshot", () => { |
| 837 | const state = makeState( | 879 | const state = makeState( |
| 838 | makeThread({ | 880 | makeThread({ |
| @@ -1078,12 +1078,16 @@ export function reducer(state: AppState, action: Action): AppState { · 1077 unmodified lines | |||
| 1078 | 1078 | ||
| 1079 | case "MARK_THREAD_VISITED": { | 1079 | case "MARK_THREAD_VISITED": { |
| 1080 | const visitedAt = action.visitedAt ?? new Date().toISOString(); | 1080 | const visitedAt = action.visitedAt ?? new Date().toISOString(); |
| 1081 | const shouldRetargetOpenDiff = state.diffOpen && state.diffThreadId !== action.threadId; | ||
| 1081 | return { | 1082 | return { |
| 1082 | ...state, | 1083 | ...state, |
| 1083 | threads: updateThread(state.threads, action.threadId, (t) => ({ | 1084 | threads: updateThread(state.threads, action.threadId, (t) => ({ |
| 1084 | ...t, | 1085 | ...t, |
| 1085 | lastVisitedAt: visitedAt, | 1086 | lastVisitedAt: visitedAt, |
| 1086 | })), | 1087 | })), |
| 1088 | diffThreadId: shouldRetargetOpenDiff ? action.threadId : state.diffThreadId, | ||
| 1089 | diffTurnId: shouldRetargetOpenDiff ? null : state.diffTurnId, | ||
| 1090 | diffFilePath: shouldRetargetOpenDiff ? null : state.diffFilePath, | ||
| 1087 | }; | 1091 | }; |
| 1088 | } | 1092 | } |
| 1089 | 1093 | ||