| author | |
| committer | |
| log | b253d1f8ca8f107933034ed76abc2cb20dbaa52b |
| tree | 7aab4114f91af36a1a8a1fc446d7c21c0f867fb5 |
| parent | 564474f92711bbd496376b8abe4ee7500c2a8d5f |
- 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 behavior3 files changed, 58 insertions(+), 1 deletions(-)
apps/web/src/components/DiffPanel.tsx+12-1| ... | ... | @@ -107,7 +107,7 @@ export default function DiffPanel({ mode = "inline" }: DiffPanelProps) { |
| 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) { |
| 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({ |
apps/web/src/store.test.ts+42| ... | ... | @@ -833,6 +833,48 @@ describe("store reducer thread continuity", () => { |
| 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({ |
apps/web/src/store.ts+4| ... | ... | @@ -1078,12 +1078,16 @@ export function reducer(state: AppState, action: Action): AppState { |
| 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 |