| 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,7 +107,7 @@ export default function DiffPanel({ mode = "inline" }: DiffPanelProps) { |
| 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) { | ... | @@ -216,6 +216,17 @@ export default function DiffPanel({ mode = "inline" }: DiffPanelProps) { |
| 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({ |
apps/web/src/store.test.ts+42| ... | @@ -833,6 +833,48 @@ describe("store reducer thread continuity", () => { | ... | @@ -833,6 +833,48 @@ describe("store reducer thread continuity", () => { |
| 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({ |
apps/web/src/store.ts+4| ... | @@ -1078,12 +1078,16 @@ export function reducer(state: AppState, action: Action): AppState { | ... | @@ -1078,12 +1078,16 @@ export function reducer(state: AppState, action: Action): AppState { |
| 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 |