authorgravatar for julius0216@outlook.comJulius Marminge <julius0216@outlook.com> 2026-02-19 01:34:11-08:00
committergravatar for julius0216@outlook.comJulius Marminge <julius0216@outlook.com> 2026-02-19 01:34:11-08:00
logb253d1f8ca8f107933034ed76abc2cb20dbaa52b
tree7aab4114f91af36a1a8a1fc446d7c21c0f867fb5
parent564474f92711bbd496376b8abe4ee7500c2a8d5f

Retarget diff panel to active route thread on visit

- 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

3 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]);
218218
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 });
835835
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 {
10781078
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 }
10891093