- 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 | 107 | const patchViewportRef = useRef<HTMLDivElement>(null); |
| 108 | 108 | const params = useParams({ strict: false }); |
| 109 | 109 | const routeThreadId = typeof params.threadId === "string" ? params.threadId : null; |
| 110 | − const activeThreadId = state.diffThreadId ?? routeThreadId; | |
| 110 | + const activeThreadId = routeThreadId ?? state.diffThreadId; | |
| 111 | 111 | const activeThread = state.threads.find((thread) => thread.id === activeThreadId); |
| 112 | 112 | const activeThreadRuntimeId = |
| 113 | 113 | activeThread?.codexThreadId ?? activeThread?.session?.threadId ?? null; |
| @@ -216,6 +216,17 @@ export default function DiffPanel({ mode = "inline" }: DiffPanelProps) { · 102 unmodified lines | ||
| 216 | 216 | target?.scrollIntoView({ block: "nearest" }); |
| 217 | 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 | 230 | const selectTurn = (turnId: string) => { |
| 220 | 231 | if (!activeThread) return; |
| 221 | 232 | dispatch({ |
| @@ -833,6 +833,48 @@ describe("store reducer thread continuity", () => { · 832 unmodified lines | ||
| 833 | 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 | 878 | it("reverts thread state to a checkpoint snapshot", () => { |
| 837 | 879 | const state = makeState( |
| 838 | 880 | makeThread({ |
| @@ -1078,12 +1078,16 @@ export function reducer(state: AppState, action: Action): AppState { · 1077 unmodified lines | ||
| 1078 | 1078 | |
| 1079 | 1079 | case "MARK_THREAD_VISITED": { |
| 1080 | 1080 | const visitedAt = action.visitedAt ?? new Date().toISOString(); |
| 1081 | + const shouldRetargetOpenDiff = state.diffOpen && state.diffThreadId !== action.threadId; | |
| 1081 | 1082 | return { |
| 1082 | 1083 | ...state, |
| 1083 | 1084 | threads: updateThread(state.threads, action.threadId, (t) => ({ |
| 1084 | 1085 | ...t, |
| 1085 | 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 | |