History

Drive diff panel state from route search params

- Parse and validate `diff`, `diffTurnId`, and `diffFilePath` from chat route search - Switch ChatView/DiffPanel diff open/target actions to router navigation instead of store actions - Remove diff-specific state/actions from the app store and update reducer tests

Julius Marminge committed 8 months ago

Commit cc3e1db1 parent b253d1f8 files · +203 −236Browse files

Unified · Split

8 files
  1. apps/web/src/components/ChatView.tsx +38 −22
  2. apps/web/src/components/DiffPanel.tsx +36 −32
  3. apps/web/src/diffRouteSearch.test.ts +53 −0
  4. apps/web/src/diffRouteSearch.ts +26 −0
  5. apps/web/src/routes/_chat.$threadId.tsx +2 −0
  6. apps/web/src/routes/_chat.tsx +46 −12
  7. apps/web/src/store.test.ts +0 −81
  8. apps/web/src/store.ts +2 −89
apps/web/src/components/ChatView.tsx+38−22
@@ -28,6 +28,7 @@ import { · 27 unmodified lines
2828 } from "react";
2929 import { useMutation, useQueries, useQuery, useQueryClient } from "@tanstack/react-query";
3030 import { useDebouncedValue } from "@tanstack/react-pacer";
31+import { useNavigate, useSearch } from "@tanstack/react-router";
3132 import { type VirtualItem, useVirtualizer } from "@tanstack/react-virtual";
3233 import { gitBranchesQueryOptions, gitCreateWorktreeMutationOptions } from "~/lib/gitReactQuery";
3334 import { checkpointDiffQueryOptions } from "~/lib/providerReactQuery";
@@ -35,6 +36,7 @@ import { projectSearchEntriesQueryOptions } from "~/lib/projectReactQuery"; · 1 unmodified line
3536 import { serverConfigQueryOptions, serverQueryKeys } from "~/lib/serverReactQuery";
3637
3738 import { isElectron } from "../env";
39+import { parseDiffRouteSearch } from "../diffRouteSearch";
3840 import { buildBootstrapInput } from "../historyBootstrap";
3941 import {
4042 type ComposerTriggerKind,
@@ -374,6 +376,8 @@ interface ChatViewProps { · 333 unmodified lines
374376
375377 export default function ChatView({ threadId }: ChatViewProps) {
376378 const { state, dispatch } = useStore();
379+ const navigate = useNavigate();
380+ const rawSearch = useSearch({ strict: false });
377381 const api = useNativeApi();
378382 const { resolvedTheme } = useTheme();
379383 const queryClient = useQueryClient();
@@ -411,6 +415,11 @@ export default function ChatView({ threadId }: ChatViewProps) { · 31 unmodified lines
411415 const checkpointTurnCountSyncFingerprintRef = useRef(new Map<string, string>());
412416
413417 const activeThread = state.threads.find((t) => t.id === threadId);
418+ const diffSearch = useMemo(
419+ () => parseDiffRouteSearch(rawSearch as Record<string, unknown>),
420+ [rawSearch],
421+ );
422+ const diffOpen = diffSearch.diff === "1";
414423 const activeThreadId = activeThread?.id ?? null;
415424 const activeSessionId = activeThread?.session?.sessionId ?? null;
416425 const activeThreadRuntimeId =
@@ -1954,32 +1963,39 @@ export default function ChatView({ threadId }: ChatViewProps) { · 1537 unmodified lines
19541963 setExpandedImage(image);
19551964 }, []);
19561965 const onToggleDiff = useCallback(() => {
1957− if (state.diffOpen) {
1958− dispatch({ type: "CLOSE_DIFF" });
1959− return;
1960− }
1961−
1962− if (!activeThread) {
1963− dispatch({ type: "TOGGLE_DIFF" });
1964− return;
1965− }
1966−
1967− dispatch({
1968− type: "OPEN_DIFF",
1969− threadId: activeThread.id,
1966+ void navigate({
1967+ to: "/$threadId",
1968+ params: { threadId },
1969+ search: (previous) => {
1970+ const {
1971+ diff: _diff,
1972+ diffTurnId: _diffTurnId,
1973+ diffFilePath: _diffFilePath,
1974+ ...rest
1975+ } = previous;
1976+ return diffOpen ? rest : { ...rest, diff: "1" };
1977+ },
19701978 });
1971− }, [activeThread, dispatch, state.diffOpen]);
1979+ }, [diffOpen, navigate, threadId]);
19721980 const onOpenTurnDiff = useCallback(
19731981 (turnId: string, filePath?: string) => {
1974− if (!activeThread) return;
1975− dispatch({
1976− type: "OPEN_DIFF",
1977− threadId: activeThread.id,
1978− turnId,
1979− ...(filePath ? { filePath } : {}),
1982+ void navigate({
1983+ to: "/$threadId",
1984+ params: { threadId },
1985+ search: (previous) => {
1986+ const {
1987+ diff: _diff,
1988+ diffTurnId: _diffTurnId,
1989+ diffFilePath: _diffFilePath,
1990+ ...rest
1991+ } = previous;
1992+ return filePath
1993+ ? { ...rest, diff: "1", diffTurnId: turnId, diffFilePath: filePath }
1994+ : { ...rest, diff: "1", diffTurnId: turnId };
1995+ },
19801996 });
19811997 },
1982− [activeThread, dispatch],
1998+ [navigate, threadId],
19831999 );
19842000 const onRevertUserMessage = useCallback(
19852001 (messageId: string) => {
@@ -2027,7 +2043,7 @@ export default function ChatView({ threadId }: ChatViewProps) { · 41 unmodified lines
20272043 keybindings={keybindings}
20282044 api={api}
20292045 gitCwd={gitCwd}
2030− diffOpen={state.diffOpen}
2046+ diffOpen={diffOpen}
20312047 onRunProjectScript={(script) => {
20322048 void runProjectScript(script);
20332049 }}
apps/web/src/components/DiffPanel.tsx+36−32
@@ -6,11 +6,12 @@ import { · 5 unmodified lines
66 WorkerPoolContextProvider,
77 } from "@pierre/diffs/react";
88 import { useQuery } from "@tanstack/react-query";
9−import { useParams } from "@tanstack/react-router";
9+import { useNavigate, useParams, useSearch } from "@tanstack/react-router";
1010 import { Columns2Icon, Rows3Icon } from "lucide-react";
1111 import { type ReactNode, useEffect, useMemo, useRef, useState } from "react";
1212 import { checkpointDiffQueryOptions } from "~/lib/providerReactQuery";
1313 import { cn } from "~/lib/utils";
14+import { parseDiffRouteSearch } from "../diffRouteSearch";
1415 import { isElectron } from "../env";
1516 import { useNativeApi } from "../hooks/useNativeApi";
1617 import { useTheme } from "../hooks/useTheme";
@@ -101,13 +102,19 @@ export function DiffWorkerPoolProvider({ children }: { children?: ReactNode }) { · 84 unmodified lines
101102
102103 export default function DiffPanel({ mode = "inline" }: DiffPanelProps) {
103104 const api = useNativeApi();
105+ const navigate = useNavigate();
104106 const { resolvedTheme } = useTheme();
105− const { state, dispatch } = useStore();
107+ const { state } = useStore();
106108 const [diffRenderMode, setDiffRenderMode] = useState<DiffRenderMode>("stacked");
107109 const patchViewportRef = useRef<HTMLDivElement>(null);
108110 const params = useParams({ strict: false });
111+ const rawSearch = useSearch({ strict: false });
109112 const routeThreadId = typeof params.threadId === "string" ? params.threadId : null;
110− const activeThreadId = routeThreadId ?? state.diffThreadId;
113+ const diffSearch = useMemo(
114+ () => parseDiffRouteSearch(rawSearch as Record<string, unknown>),
115+ [rawSearch],
116+ );
117+ const activeThreadId = routeThreadId;
111118 const activeThread = state.threads.find((thread) => thread.id === activeThreadId);
112119 const activeThreadRuntimeId =
113120 activeThread?.codexThreadId ?? activeThread?.session?.threadId ?? null;
@@ -115,10 +122,8 @@ export default function DiffPanel({ mode = "inline" }: DiffPanelProps) { · 1 unmodified line
115122 const { turnDiffSummaries, inferredCheckpointTurnCountByTurnId } =
116123 useTurnDiffSummaries(activeThread);
117124
118− const canApplyStoredTarget = Boolean(activeThread && state.diffThreadId === activeThread.id);
119− const selectedTurnId = canApplyStoredTarget ? state.diffTurnId : null;
120− const selectedFilePath =
121− canApplyStoredTarget && selectedTurnId !== null ? state.diffFilePath : null;
125+ const selectedTurnId = diffSearch.diffTurnId ?? null;
126+ const selectedFilePath = selectedTurnId !== null ? diffSearch.diffFilePath ?? null : null;
122127 const selectedTurn =
123128 selectedTurnId === null
124129 ? undefined
@@ -216,30 +221,36 @@ export default function DiffPanel({ mode = "inline" }: DiffPanelProps) { · 91 unmodified lines
216221 target?.scrollIntoView({ block: "nearest" });
217222 }, [selectedFilePath, renderableFiles]);
218223
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−
230224 const selectTurn = (turnId: string) => {
231225 if (!activeThread) return;
232− dispatch({
233− type: "SET_DIFF_TARGET",
234− threadId: activeThread.id,
235− turnId,
226+ void navigate({
227+ to: "/$threadId",
228+ params: { threadId: activeThread.id },
229+ search: (previous) => {
230+ const {
231+ diff: _diff,
232+ diffTurnId: _diffTurnId,
233+ diffFilePath: _diffFilePath,
234+ ...rest
235+ } = previous;
236+ return { ...rest, diff: "1", diffTurnId: turnId };
237+ },
236238 });
237239 };
238240 const selectWholeConversation = () => {
239241 if (!activeThread) return;
240− dispatch({
241− type: "SET_DIFF_TARGET",
242− threadId: activeThread.id,
242+ void navigate({
243+ to: "/$threadId",
244+ params: { threadId: activeThread.id },
245+ search: (previous) => {
246+ const {
247+ diff: _diff,
248+ diffTurnId: _diffTurnId,
249+ diffFilePath: _diffFilePath,
250+ ...rest
251+ } = previous;
252+ return { ...rest, diff: "1" };
253+ },
243254 });
244255 };
245256
@@ -329,13 +340,6 @@ export default function DiffPanel({ mode = "inline" }: DiffPanelProps) { · 83 unmodified lines
329340 ) : (
330341 <>
331342 <div ref={patchViewportRef} className="min-h-0 min-w-0 flex-1 overflow-hidden">
332− {!canApplyStoredTarget && state.diffThreadId && (
333− <div className="px-3 pt-2">
334− <p className="mb-2 text-[11px] text-muted-foreground/65">
335− Showing diffs for the active thread.
336− </p>
337− </div>
338− )}
339343 {checkpointDiffError && !renderablePatch && (
340344 <div className="px-3">
341345 <p className="mb-2 text-[11px] text-red-500/80">{checkpointDiffError}</p>
apps/web/src/diffRouteSearch.test.ts+53−0
@@ -0,0 +1,53 @@
1+import { describe, expect, it } from "vitest";
2+
3+import { parseDiffRouteSearch } from "./diffRouteSearch";
4+
5+describe("parseDiffRouteSearch", () => {
6+ it("parses valid diff search values", () => {
7+ const parsed = parseDiffRouteSearch({
8+ diff: "1",
9+ diffTurnId: "turn-1",
10+ diffFilePath: "src/app.ts",
11+ });
12+
13+ expect(parsed).toEqual({
14+ diff: "1",
15+ diffTurnId: "turn-1",
16+ diffFilePath: "src/app.ts",
17+ });
18+ });
19+
20+ it("drops turn and file values when diff is closed", () => {
21+ const parsed = parseDiffRouteSearch({
22+ diff: "0",
23+ diffTurnId: "turn-1",
24+ diffFilePath: "src/app.ts",
25+ });
26+
27+ expect(parsed).toEqual({});
28+ });
29+
30+ it("drops file value when turn is not selected", () => {
31+ const parsed = parseDiffRouteSearch({
32+ diff: "1",
33+ diffFilePath: "src/app.ts",
34+ });
35+
36+ expect(parsed).toEqual({
37+ diff: "1",
38+ });
39+ });
40+
41+ it("normalizes whitespace-only values", () => {
42+ const parsed = parseDiffRouteSearch({
43+ diff: "1",
44+ diffTurnId: " ",
45+ diffFilePath: " ",
46+ });
47+
48+ expect(parsed).toEqual({
49+ diff: "1",
50+ });
51+ });
52+});
53+
apps/web/src/diffRouteSearch.ts+26−0
@@ -0,0 +1,26 @@
1+export interface DiffRouteSearch {
2+ diff?: "1";
3+ diffTurnId?: string;
4+ diffFilePath?: string;
5+}
6+
7+function normalizeSearchString(value: unknown): string | undefined {
8+ if (typeof value !== "string") {
9+ return undefined;
10+ }
11+ const normalized = value.trim();
12+ return normalized.length > 0 ? normalized : undefined;
13+}
14+
15+export function parseDiffRouteSearch(search: Record<string, unknown>): DiffRouteSearch {
16+ const diff = search.diff === "1" ? "1" : undefined;
17+ const diffTurnId = diff ? normalizeSearchString(search.diffTurnId) : undefined;
18+ const diffFilePath = diff && diffTurnId ? normalizeSearchString(search.diffFilePath) : undefined;
19+
20+ return {
21+ ...(diff ? { diff } : {}),
22+ ...(diffTurnId ? { diffTurnId } : {}),
23+ ...(diffFilePath ? { diffFilePath } : {}),
24+ };
25+}
26+
apps/web/src/routes/_chat.$threadId.tsx+2−0
@@ -2,6 +2,7 @@ import { createFileRoute, useNavigate } from "@tanstack/react-router"; · 1 unmodified line
22 import { useEffect } from "react";
33
44 import ChatView from "../components/ChatView";
5+import { parseDiffRouteSearch } from "../diffRouteSearch";
56 import { useStore } from "../store";
67
78 function ChatThreadRouteView() {
@@ -34,5 +35,6 @@ function ChatThreadRouteView() { · 26 unmodified lines
3435 }
3536
3637 export const Route = createFileRoute("/_chat/$threadId")({
38+ validateSearch: (search) => parseDiffRouteSearch(search),
3739 component: ChatThreadRouteView,
3840 });
apps/web/src/routes/_chat.tsx+46−12
@@ -1,9 +1,15 @@
1−import { Outlet, createFileRoute } from "@tanstack/react-router";
2−import { Activity, Suspense, lazy, type ReactNode } from "react";
1+import {
2+ Outlet,
3+ createFileRoute,
4+ useNavigate,
5+ useParams,
6+ useSearch,
7+} from "@tanstack/react-router";
8+import { Activity, Suspense, lazy, type ReactNode, useCallback, useMemo } from "react";
39
410 import Sidebar from "../components/Sidebar";
11+import { parseDiffRouteSearch } from "../diffRouteSearch";
512 import { useMediaQuery } from "../hooks/useMediaQuery";
6−import { useStore } from "../store";
713 import { Sheet, SheetPopup } from "../components/ui/sheet";
814
915 const DiffPanel = lazy(() => import("../components/DiffPanel"));
@@ -14,15 +20,19 @@ const DiffWorkerPoolProvider = lazy(() => · 4 unmodified lines
1420 );
1521 const DIFF_INLINE_LAYOUT_MEDIA_QUERY = "(max-width: 1180px)";
1622
17−const DiffPanelWrapper = (props: { children: ReactNode; sheet: boolean }) => {
18− const { state, dispatch } = useStore();
23+const DiffPanelWrapper = (props: {
24+ children: ReactNode;
25+ sheet: boolean;
26+ diffOpen: boolean;
27+ onCloseDiff: () => void;
28+}) => {
1929 if (props.sheet) {
2030 return (
2131 <Sheet
22− open={state.diffOpen}
32+ open={props.diffOpen}
2333 onOpenChange={(open) => {
2434 if (!open) {
25− dispatch({ type: "CLOSE_DIFF" });
35+ props.onCloseDiff();
2636 }
2737 }}
2838 >
@@ -39,18 +49,42 @@ const DiffPanelWrapper = (props: { children: ReactNode; sheet: boolean }) => { · 10 unmodified lines
3949 }
4050
4151 return (
42− <aside className={state.diffOpen ? undefined : "hidden"} aria-hidden={!state.diffOpen}>
52+ <aside className={props.diffOpen ? undefined : "hidden"} aria-hidden={!props.diffOpen}>
4353 {props.children}
4454 </aside>
4555 );
4656 };
4757
4858 function ChatRouteLayout() {
49− const { state } = useStore();
59+ const navigate = useNavigate();
60+ const params = useParams({ strict: false });
61+ const routeThreadId = typeof params.threadId === "string" ? params.threadId : null;
62+ const rawSearch = useSearch({ strict: false });
63+ const diffSearch = useMemo(
64+ () => parseDiffRouteSearch(rawSearch as Record<string, unknown>),
65+ [rawSearch],
66+ );
67+ const diffOpen = routeThreadId !== null && diffSearch.diff === "1";
5068 const shouldUseDiffSheet = useMediaQuery(DIFF_INLINE_LAYOUT_MEDIA_QUERY);
69+ const closeDiff = useCallback(() => {
70+ if (!routeThreadId) return;
71+ void navigate({
72+ to: "/$threadId",
73+ params: { threadId: routeThreadId },
74+ search: (previous) => {
75+ const {
76+ diff: _diff,
77+ diffTurnId: _diffTurnId,
78+ diffFilePath: _diffFilePath,
79+ ...rest
80+ } = previous;
81+ return rest;
82+ },
83+ });
84+ }, [navigate, routeThreadId]);
5185
5286 const diffLoadingFallback =
53− !state.diffOpen || shouldUseDiffSheet ? (
87+ !diffOpen || shouldUseDiffSheet ? (
5488 <div className="flex h-full min-h-0 items-center justify-center px-4 text-center text-xs text-muted-foreground/70">
5589 Loading diff viewer...
5690 </div>
@@ -64,8 +98,8 @@ function ChatRouteLayout() { · 7 unmodified lines
6498 <div className="flex h-screen overflow-hidden bg-background text-foreground isolate">
6599 <Sidebar />
66100 <Outlet />
67− <Activity mode={state.diffOpen ? "visible" : "hidden"}>
68− <DiffPanelWrapper sheet={shouldUseDiffSheet}>
101+ <Activity mode={diffOpen ? "visible" : "hidden"}>
102+ <DiffPanelWrapper sheet={shouldUseDiffSheet} diffOpen={diffOpen} onCloseDiff={closeDiff}>
69103 <Suspense fallback={diffLoadingFallback}>
70104 <DiffWorkerPoolProvider>
71105 <DiffPanel mode={shouldUseDiffSheet ? "sheet" : "inline"} />
apps/web/src/store.test.ts+0−81
@@ -81,10 +81,6 @@ function makeState(thread: Thread): AppState { · 80 unmodified lines
8181 threads: [thread],
8282 threadsHydrated: true,
8383 runtimeMode: "full-access",
84− diffOpen: false,
85− diffThreadId: null,
86− diffTurnId: null,
87− diffFilePath: null,
8884 };
8985 }
9086
@@ -149,21 +145,6 @@ describe("store reducer thread continuity", () => { · 58 unmodified lines
149145 expect(next.threads[0]?.terminalOpen).toBe(true);
150146 });
151147
152− it("opens diff panel with an explicit turn/file target", () => {
153− const state = makeState(makeThread());
154− const next = reducer(state, {
155− type: "OPEN_DIFF",
156− threadId: "thread-local-1",
157− turnId: "turn-1",
158− filePath: "src/app.ts",
159− });
160−
161− expect(next.diffOpen).toBe(true);
162− expect(next.diffThreadId).toBe("thread-local-1");
163− expect(next.diffTurnId).toBe("turn-1");
164− expect(next.diffFilePath).toBe("src/app.ts");
165− });
166−
167148 it("sets terminal open state per thread", () => {
168149 const state = makeState(makeThread({ terminalOpen: true }));
169150 const next = reducer(state, {
@@ -618,10 +599,6 @@ describe("store reducer thread continuity", () => { · 448 unmodified lines
618599 ],
619600 threadsHydrated: true,
620601 runtimeMode: "full-access",
621− diffOpen: true,
622− diffThreadId: "thread-b",
623− diffTurnId: "turn-b",
624− diffFilePath: "src/removed.ts",
625602 };
626603
627604 const next = reducer(state, {
@@ -645,10 +622,6 @@ describe("store reducer thread continuity", () => { · 17 unmodified lines
645622 expect(next.threads).toHaveLength(1);
646623 expect(next.threads[0]?.id).toBe("thread-a");
647624 expect(next.threads[0]?.projectId).toBe("project-new-a");
648− expect(next.diffOpen).toBe(false);
649− expect(next.diffThreadId).toBeNull();
650− expect(next.diffTurnId).toBeNull();
651− expect(next.diffFilePath).toBeNull();
652625 });
653626
654627 it("treats empty scripts from sync as authoritative", () => {
@@ -674,10 +647,6 @@ describe("store reducer thread continuity", () => { · 19 unmodified lines
674647 threads: [makeThread({ id: "thread-a", projectId: "project-old-a" })],
675648 threadsHydrated: true,
676649 runtimeMode: "full-access",
677− diffOpen: false,
678− diffThreadId: null,
679− diffTurnId: null,
680− diffFilePath: null,
681650 };
682651
683652 const next = reducer(state, {
@@ -766,10 +735,6 @@ describe("store reducer thread continuity", () => { · 82 unmodified lines
766735 ],
767736 threadsHydrated: true,
768737 runtimeMode: "full-access",
769− diffOpen: true,
770− diffThreadId: "thread-a",
771− diffTurnId: "turn-a",
772− diffFilePath: "src/a.ts",
773738 };
774739
775740 const next = reducer(state, {
@@ -781,10 +746,6 @@ describe("store reducer thread continuity", () => { · 5 unmodified lines
781746 expect(next.projects[0]?.id).toBe("project-2");
782747 expect(next.threads).toHaveLength(1);
783748 expect(next.threads[0]?.id).toBe("thread-b");
784− expect(next.diffOpen).toBe(false);
785− expect(next.diffThreadId).toBeNull();
786− expect(next.diffTurnId).toBeNull();
787− expect(next.diffFilePath).toBeNull();
788749 });
789750
790751 it("marks completion as seen immediately for the active thread", () => {
@@ -833,48 +794,6 @@ describe("store reducer thread continuity", () => { · 42 unmodified lines
833794 expect(next.threads[0]?.lastVisitedAt).toBe(nextVisitedAt);
834795 });
835796
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−
878797 it("reverts thread state to a checkpoint snapshot", () => {
879798 const state = makeState(
880799 makeThread({
apps/web/src/store.ts+2−89
@@ -48,20 +48,6 @@ type Action = · 47 unmodified lines
4848 | { type: "NEW_THREAD_TERMINAL"; threadId: string; terminalId: string }
4949 | { type: "SET_THREAD_ACTIVE_TERMINAL"; threadId: string; terminalId: string }
5050 | { type: "CLOSE_THREAD_TERMINAL"; threadId: string; terminalId: string }
51− | { type: "TOGGLE_DIFF" }
52− | {
53− type: "OPEN_DIFF";
54− threadId: string;
55− turnId?: string;
56− filePath?: string;
57− }
58− | {
59− type: "SET_DIFF_TARGET";
60− threadId: string;
61− turnId?: string;
62− filePath?: string;
63− }
64− | { type: "CLOSE_DIFF" }
6551 | {
6652 type: "APPLY_EVENT";
6753 event: ProviderEvent;
@@ -110,10 +96,6 @@ export interface AppState { · 42 unmodified lines
11096 threads: Thread[];
11197 threadsHydrated: boolean;
11298 runtimeMode: RuntimeMode;
113− diffOpen: boolean;
114− diffThreadId: string | null;
115− diffTurnId: string | null;
116− diffFilePath: string | null;
11799 }
118100
119101 const PERSISTED_STATE_KEY = "t3code:renderer-state:v7";
@@ -133,10 +115,6 @@ const initialState: AppState = { · 13 unmodified lines
133115 threads: [],
134116 threadsHydrated: false,
135117 runtimeMode: DEFAULT_RUNTIME_MODE,
136− diffOpen: false,
137− diffThreadId: null,
138− diffTurnId: null,
139− diffFilePath: null,
140118 };
141119
142120 // ── Helpers ──────────────────────────────────────────────────────────
@@ -160,10 +138,6 @@ function readPersistedState(): AppState { · 17 unmodified lines
160138 ...hydrated,
161139 threads,
162140 threadsHydrated: threads.length > 0,
163− diffOpen: false,
164− diffThreadId: null,
165− diffTurnId: null,
166− diffFilePath: null,
167141 };
168142 } catch {
169143 return initialState;
@@ -191,32 +165,6 @@ function updateThread( · 21 unmodified lines
191165 return threads.map((t) => (t.id === threadId ? updater(t) : t));
192166 }
193167
194−function resetDiffTargetIfMissing(state: AppState, threads: Thread[]) {
195− if (!state.diffThreadId) {
196− return {
197− diffOpen: state.diffOpen,
198− diffThreadId: state.diffThreadId,
199− diffTurnId: state.diffTurnId,
200− diffFilePath: state.diffFilePath,
201− };
202− }
203− const hasThread = threads.some((thread) => thread.id === state.diffThreadId);
204− if (hasThread) {
205− return {
206− diffOpen: state.diffOpen,
207− diffThreadId: state.diffThreadId,
208− diffTurnId: state.diffTurnId,
209− diffFilePath: state.diffFilePath,
210− };
211− }
212− return {
213− diffOpen: false,
214− diffThreadId: null,
215− diffTurnId: null,
216− diffFilePath: null,
217− };
218−}
219−
220168 function mergeTurnDiffSummaries(
221169 existing: Thread["turnDiffSummaries"],
222170 next: Thread["turnDiffSummaries"],
@@ -615,13 +563,11 @@ export function reducer(state: AppState, action: Action): AppState { · 392 unmodified lines
615563 });
616564 })
617565 .filter((thread): thread is Thread => thread !== null);
618− const diffState = resetDiffTargetIfMissing(state, nextThreads);
619566
620567 return {
621568 ...state,
622569 projects: nextProjects,
623570 threads: nextThreads,
624− ...diffState,
625571 };
626572 }
627573
@@ -640,13 +586,11 @@ export function reducer(state: AppState, action: Action): AppState { · 12 unmodified lines
640586 }
641587
642588 const threads = state.threads.filter((thread) => thread.projectId !== action.projectId);
643− const diffState = resetDiffTargetIfMissing(state, threads);
644589
645590 return {
646591 ...state,
647592 projects,
648593 threads,
649− ...diffState,
650594 };
651595 }
652596
@@ -830,29 +774,6 @@ export function reducer(state: AppState, action: Action): AppState { · 177 unmodified lines
830774 ),
831775 };
832776
833− case "TOGGLE_DIFF":
834− return { ...state, diffOpen: !state.diffOpen };
835−
836− case "OPEN_DIFF":
837− return {
838− ...state,
839− diffOpen: true,
840− diffThreadId: action.threadId,
841− diffTurnId: action.turnId ?? null,
842− diffFilePath: action.filePath ?? null,
843− };
844−
845− case "SET_DIFF_TARGET":
846− return {
847− ...state,
848− diffThreadId: action.threadId,
849− diffTurnId: action.turnId ?? null,
850− diffFilePath: action.filePath ?? null,
851− };
852−
853− case "CLOSE_DIFF":
854− return { ...state, diffOpen: false };
855−
856777 case "APPLY_TERMINAL_EVENT":
857778 if (!state.threads.some((thread) => thread.id === action.event.threadId)) {
858779 return state;
@@ -1078,16 +999,12 @@ export function reducer(state: AppState, action: Action): AppState { · 219 unmodified lines
1078999
10791000 case "MARK_THREAD_VISITED": {
10801001 const visitedAt = action.visitedAt ?? new Date().toISOString();
1081− const shouldRetargetOpenDiff = state.diffOpen && state.diffThreadId !== action.threadId;
10821002 return {
10831003 ...state,
10841004 threads: updateThread(state.threads, action.threadId, (t) => ({
10851005 ...t,
10861006 lastVisitedAt: visitedAt,
10871007 })),
1088− diffThreadId: shouldRetargetOpenDiff ? action.threadId : state.diffThreadId,
1089− diffTurnId: shouldRetargetOpenDiff ? null : state.diffTurnId,
1090− diffFilePath: shouldRetargetOpenDiff ? null : state.diffFilePath,
10911008 };
10921009 }
10931010
@@ -1097,15 +1014,11 @@ export function reducer(state: AppState, action: Action): AppState { · 3 unmodified lines
10971014 runtimeMode: action.mode,
10981015 };
10991016
1100− case "DELETE_THREAD": {
1101− const threads = state.threads.filter((t) => t.id !== action.threadId);
1102− const diffState = resetDiffTargetIfMissing(state, threads);
1017+ case "DELETE_THREAD":
11031018 return {
11041019 ...state,
1105− threads,
1106− ...diffState,
1020+ threads: state.threads.filter((t) => t.id !== action.threadId),
11071021 };
1108− }
11091022
11101023 default:
11111024 return state;

Keyboard shortcuts

General

Show keyboard shortcuts
?
Focus the current search
/

Issues and pull requests

Open filters
f

Issues, pull requests and Inbox

Select next item
j↓
Select previous item
k↑
Open selected item
Enter
Dismiss filters or clear selection
Esc

Repository code

Go to file
t