authorgravatar for julius0216@outlook.comJulius Marminge <julius0216@outlook.com> 2026-02-19 15:43:24-08:00
committergravatar for noreply@github.comGitHub <noreply@github.com> 2026-02-19 15:43:24-08:00
log70a13c298f50be8c620020708d8b1db6ae80557e
treeb1cf20836e92b09979dca45a58d916cc162b188a
parent8cea075a1eb94b574434676122abc5f82e941f43
parent8a72a2a91d52c16f3f761115d1c49b21fa6cdc4e
signature Signed by PGP key B5690EEEBB952194

Merge pull request #78 from pingdotgg/codething/53bdee92

Retarget open diff panel to the visited thread

9 files changed, 272 insertions(+), 270 deletions(-)

apps/web/src/components/ChatView.tsx+46-28
......@@ -28,6 +28,7 @@ import {
2828} from "react";
2929import { useMutation, useQueries, useQuery, useQueryClient } from "@tanstack/react-query";
3030import { useDebouncedValue } from "@tanstack/react-pacer";
31import { useNavigate, useSearch } from "@tanstack/react-router";
3132import { type VirtualItem, useVirtualizer } from "@tanstack/react-virtual";
3233import { gitBranchesQueryOptions, gitCreateWorktreeMutationOptions } from "~/lib/gitReactQuery";
3334import { checkpointDiffQueryOptions } from "~/lib/providerReactQuery";
......@@ -35,6 +36,7 @@ import { projectSearchEntriesQueryOptions } from "~/lib/projectReactQuery";
3536import { serverConfigQueryOptions, serverQueryKeys } from "~/lib/serverReactQuery";
3637
3738import { isElectron } from "../env";
39import { parseDiffRouteSearch, stripDiffSearchParams } from "../diffRouteSearch";
3840import { useAppSettings } from "../appSettings";
3941import { buildBootstrapInput } from "../historyBootstrap";
4042import {
......@@ -375,6 +377,8 @@ interface ChatViewProps {
375377
376378export default function ChatView({ threadId }: ChatViewProps) {
377379 const { state, dispatch } = useStore();
380 const navigate = useNavigate();
381 const rawSearch = useSearch({ strict: false });
378382 const api = useNativeApi();
379383 const { resolvedTheme } = useTheme();
380384 const { settings: appSettings } = useAppSettings();
......@@ -413,6 +417,11 @@ export default function ChatView({ threadId }: ChatViewProps) {
413417 const checkpointTurnCountSyncFingerprintRef = useRef(new Map<string, string>());
414418
415419 const activeThread = state.threads.find((t) => t.id === threadId);
420 const diffSearch = useMemo(
421 () => parseDiffRouteSearch(rawSearch as Record<string, unknown>),
422 [rawSearch],
423 );
424 const diffOpen = diffSearch.diff === "1";
416425 const activeThreadId = activeThread?.id ?? null;
417426 const activeSessionId = activeThread?.session?.sessionId ?? null;
418427 const activeThreadRuntimeId =
......@@ -1962,32 +1971,29 @@ export default function ChatView({ threadId }: ChatViewProps) {
19621971 setExpandedImage(image);
19631972 }, []);
19641973 const onToggleDiff = useCallback(() => {
1965 if (state.diffOpen) {
1966 dispatch({ type: "CLOSE_DIFF" });
1967 return;
1968 }
1969
1970 if (!activeThread) {
1971 dispatch({ type: "TOGGLE_DIFF" });
1972 return;
1973 }
1974
1975 dispatch({
1976 type: "OPEN_DIFF",
1977 threadId: activeThread.id,
1974 void navigate({
1975 to: "/$threadId",
1976 params: { threadId },
1977 search: (previous) => {
1978 const rest = stripDiffSearchParams(previous);
1979 return diffOpen ? rest : { ...rest, diff: "1" };
1980 },
19781981 });
1979 }, [activeThread, dispatch, state.diffOpen]);
1982 }, [diffOpen, navigate, threadId]);
19801983 const onOpenTurnDiff = useCallback(
19811984 (turnId: string, filePath?: string) => {
1982 if (!activeThread) return;
1983 dispatch({
1984 type: "OPEN_DIFF",
1985 threadId: activeThread.id,
1986 turnId,
1987 ...(filePath ? { filePath } : {}),
1985 void navigate({
1986 to: "/$threadId",
1987 params: { threadId },
1988 search: (previous) => {
1989 const rest = stripDiffSearchParams(previous);
1990 return filePath
1991 ? { ...rest, diff: "1", diffTurnId: turnId, diffFilePath: filePath }
1992 : { ...rest, diff: "1", diffTurnId: turnId };
1993 },
19881994 });
19891995 },
1990 [activeThread, dispatch],
1996 [navigate, threadId],
19911997 );
19921998 const onRevertUserMessage = useCallback(
19931999 (messageId: string) => {
......@@ -2035,7 +2041,7 @@ export default function ChatView({ threadId }: ChatViewProps) {
20352041 keybindings={keybindings}
20362042 api={api}
20372043 gitCwd={gitCwd}
2038 diffOpen={state.diffOpen}
2044 diffOpen={diffOpen}
20392045 onRunProjectScript={(script) => {
20402046 void runProjectScript(script);
20412047 }}
......@@ -2820,11 +2826,16 @@ const MessagesTimeline = memo(function MessagesTimeline({
28202826 row.message.id,
28212827 );
28222828 if (!turnSummary) return null;
2823 const checkpointFilesState = turnCheckpointFilesByTurnId.get(turnSummary.turnId);
2829 const checkpointFilesState = turnCheckpointFilesByTurnId.get(
2830 turnSummary.turnId,
2831 );
28242832 const checkpointFiles = checkpointFilesState?.files ?? [];
28252833 const summaryStat = checkpointFiles.reduce(
28262834 (acc, file) => {
2827 if (typeof file.additions !== "number" || typeof file.deletions !== "number") {
2835 if (
2836 typeof file.additions !== "number" ||
2837 typeof file.deletions !== "number"
2838 ) {
28282839 return acc;
28292840 }
28302841 return {
......@@ -2835,7 +2846,9 @@ const MessagesTimeline = memo(function MessagesTimeline({
28352846 { additions: 0, deletions: 0 },
28362847 );
28372848 const changedFileCountLabel =
2838 checkpointFilesState?.status === "ready" ? String(checkpointFiles.length) : "...";
2849 checkpointFilesState?.status === "ready"
2850 ? String(checkpointFiles.length)
2851 : "...";
28392852 return (
28402853 <div className="mt-2 rounded-lg border border-border/80 bg-card/45 p-2.5">
28412854 <div className="mb-1.5 flex items-center justify-between gap-2">
......@@ -2846,7 +2859,9 @@ const MessagesTimeline = memo(function MessagesTimeline({
28462859 <span className="mx-1">•</span>
28472860 <span className="text-success">+{summaryStat.additions}</span>
28482861 <span className="mx-0.5 text-muted-foreground/70">/</span>
2849 <span className="text-destructive">-{summaryStat.deletions}</span>
2862 <span className="text-destructive">
2863 -{summaryStat.deletions}
2864 </span>
28502865 </>
28512866 )}
28522867 </p>
......@@ -2854,12 +2869,15 @@ const MessagesTimeline = memo(function MessagesTimeline({
28542869 type="button"
28552870 size="xs"
28562871 variant="outline"
2857 onClick={() => onOpenTurnDiff(turnSummary.turnId, checkpointFiles[0]?.path)}
2872 onClick={() =>
2873 onOpenTurnDiff(turnSummary.turnId, checkpointFiles[0]?.path)
2874 }
28582875 >
28592876 View diff
28602877 </Button>
28612878 </div>
2862 {!checkpointFilesState || checkpointFilesState.status === "pending" ? (
2879 {!checkpointFilesState ||
2880 checkpointFilesState.status === "pending" ? (
28632881 <p className="text-[11px] text-muted-foreground/70">
28642882 Waiting for checkpoint metadata...
28652883 </p>
apps/web/src/components/DiffPanel.tsx+29-50
......@@ -1,16 +1,12 @@
11import { parsePatchFiles } from "@pierre/diffs";
2import {
3 FileDiff,
4 type FileDiffMetadata,
5 Virtualizer,
6 WorkerPoolContextProvider,
7} from "@pierre/diffs/react";
2import { FileDiff, type FileDiffMetadata, Virtualizer } from "@pierre/diffs/react";
83import { useQuery } from "@tanstack/react-query";
9import { useParams } from "@tanstack/react-router";
4import { useNavigate, useParams, useSearch } from "@tanstack/react-router";
105import { Columns2Icon, Rows3Icon } from "lucide-react";
11import { type ReactNode, useEffect, useMemo, useRef, useState } from "react";
6import { useEffect, useMemo, useRef, useState } from "react";
127import { checkpointDiffQueryOptions } from "~/lib/providerReactQuery";
138import { cn } from "~/lib/utils";
9import { parseDiffRouteSearch, stripDiffSearchParams } from "../diffRouteSearch";
1410import { isElectron } from "../env";
1511import { useNativeApi } from "../hooks/useNativeApi";
1612import { useTheme } from "../hooks/useTheme";
......@@ -76,38 +72,23 @@ interface DiffPanelProps {
7672 mode?: "inline" | "sheet";
7773}
7874
79export function DiffWorkerPoolProvider({ children }: { children?: ReactNode }) {
80 const workerPoolSize = useMemo(() => {
81 const cores = typeof navigator === "undefined" ? 4 : Math.max(1, navigator.hardwareConcurrency || 4);
82 return Math.max(2, Math.min(6, Math.floor(cores / 2)));
83 }, []);
84
85 return (
86 <WorkerPoolContextProvider
87 poolOptions={{
88 workerFactory: () =>
89 new Worker(new URL("../workers/diffs.worker.ts", import.meta.url), { type: "module" }),
90 poolSize: workerPoolSize,
91 totalASTLRUCacheSize: 240,
92 }}
93 highlighterOptions={{
94 tokenizeMaxLineLength: 1_000,
95 }}
96 >
97 {children}
98 </WorkerPoolContextProvider>
99 );
100}
75export { DiffWorkerPoolProvider } from "./DiffWorkerPoolProvider";
10176
10277export default function DiffPanel({ mode = "inline" }: DiffPanelProps) {
10378 const api = useNativeApi();
79 const navigate = useNavigate();
10480 const { resolvedTheme } = useTheme();
105 const { state, dispatch } = useStore();
81 const { state } = useStore();
10682 const [diffRenderMode, setDiffRenderMode] = useState<DiffRenderMode>("stacked");
10783 const patchViewportRef = useRef<HTMLDivElement>(null);
10884 const params = useParams({ strict: false });
85 const rawSearch = useSearch({ strict: false });
10986 const routeThreadId = typeof params.threadId === "string" ? params.threadId : null;
110 const activeThreadId = state.diffThreadId ?? routeThreadId;
87 const diffSearch = useMemo(
88 () => parseDiffRouteSearch(rawSearch as Record<string, unknown>),
89 [rawSearch],
90 );
91 const activeThreadId = routeThreadId;
11192 const activeThread = state.threads.find((thread) => thread.id === activeThreadId);
11293 const activeThreadRuntimeId =
11394 activeThread?.codexThreadId ?? activeThread?.session?.threadId ?? null;
......@@ -115,10 +96,8 @@ export default function DiffPanel({ mode = "inline" }: DiffPanelProps) {
11596 const { turnDiffSummaries, inferredCheckpointTurnCountByTurnId } =
11697 useTurnDiffSummaries(activeThread);
11798
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;
99 const selectedTurnId = diffSearch.diffTurnId ?? null;
100 const selectedFilePath = selectedTurnId !== null ? (diffSearch.diffFilePath ?? null) : null;
122101 const selectedTurn =
123102 selectedTurnId === null
124103 ? undefined
......@@ -218,17 +197,24 @@ export default function DiffPanel({ mode = "inline" }: DiffPanelProps) {
218197
219198 const selectTurn = (turnId: string) => {
220199 if (!activeThread) return;
221 dispatch({
222 type: "SET_DIFF_TARGET",
223 threadId: activeThread.id,
224 turnId,
200 void navigate({
201 to: "/$threadId",
202 params: { threadId: activeThread.id },
203 search: (previous) => {
204 const rest = stripDiffSearchParams(previous);
205 return { ...rest, diff: "1", diffTurnId: turnId };
206 },
225207 });
226208 };
227209 const selectWholeConversation = () => {
228210 if (!activeThread) return;
229 dispatch({
230 type: "SET_DIFF_TARGET",
231 threadId: activeThread.id,
211 void navigate({
212 to: "/$threadId",
213 params: { threadId: activeThread.id },
214 search: (previous) => {
215 const rest = stripDiffSearchParams(previous);
216 return { ...rest, diff: "1" };
217 },
232218 });
233219 };
234220
......@@ -318,13 +304,6 @@ export default function DiffPanel({ mode = "inline" }: DiffPanelProps) {
318304 ) : (
319305 <>
320306 <div ref={patchViewportRef} className="min-h-0 min-w-0 flex-1 overflow-hidden">
321 {!canApplyStoredTarget && state.diffThreadId && (
322 <div className="px-3 pt-2">
323 <p className="mb-2 text-[11px] text-muted-foreground/65">
324 Showing diffs for the active thread.
325 </p>
326 </div>
327 )}
328307 {checkpointDiffError && !renderablePatch && (
329308 <div className="px-3">
330309 <p className="mb-2 text-[11px] text-red-500/80">{checkpointDiffError}</p>
apps/web/src/components/DiffWorkerPoolProvider.tsx created+26
......@@ -0,0 +1,26 @@
1import { WorkerPoolContextProvider } from "@pierre/diffs/react";
2import { useMemo, type ReactNode } from "react";
3
4export function DiffWorkerPoolProvider({ children }: { children?: ReactNode }) {
5 const workerPoolSize = useMemo(() => {
6 const cores = typeof navigator === "undefined" ? 4 : Math.max(1, navigator.hardwareConcurrency || 4);
7 return Math.max(2, Math.min(6, Math.floor(cores / 2)));
8 }, []);
9
10 return (
11 <WorkerPoolContextProvider
12 poolOptions={{
13 workerFactory: () =>
14 new Worker(new URL("../workers/diffs.worker.ts", import.meta.url), { type: "module" }),
15 poolSize: workerPoolSize,
16 totalASTLRUCacheSize: 240,
17 }}
18 highlighterOptions={{
19 tokenizeMaxLineLength: 1_000,
20 }}
21 >
22 {children}
23 </WorkerPoolContextProvider>
24 );
25}
26
apps/web/src/diffRouteSearch.test.ts created+53
......@@ -0,0 +1,53 @@
1import { describe, expect, it } from "vitest";
2
3import { parseDiffRouteSearch } from "./diffRouteSearch";
4
5describe("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 created+32
......@@ -0,0 +1,32 @@
1export interface DiffRouteSearch {
2 diff?: "1";
3 diffTurnId?: string;
4 diffFilePath?: string;
5}
6
7function 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
15export function stripDiffSearchParams<T extends Record<string, unknown>>(
16 params: T,
17): Omit<T, "diff" | "diffTurnId" | "diffFilePath"> {
18 const { diff: _diff, diffTurnId: _diffTurnId, diffFilePath: _diffFilePath, ...rest } = params;
19 return rest as Omit<T, "diff" | "diffTurnId" | "diffFilePath">;
20}
21
22export function parseDiffRouteSearch(search: Record<string, unknown>): DiffRouteSearch {
23 const diff = search.diff === "1" ? "1" : undefined;
24 const diffTurnId = diff ? normalizeSearchString(search.diffTurnId) : undefined;
25 const diffFilePath = diff && diffTurnId ? normalizeSearchString(search.diffFilePath) : undefined;
26
27 return {
28 ...(diff ? { diff } : {}),
29 ...(diffTurnId ? { diffTurnId } : {}),
30 ...(diffFilePath ? { diffFilePath } : {}),
31 };
32}
apps/web/src/routes/_chat.$threadId.tsx+78-2
......@@ -1,14 +1,67 @@
11import { createFileRoute, useNavigate } from "@tanstack/react-router";
2import { useEffect } from "react";
2import { Activity, Suspense, lazy, type ReactNode, useCallback, useEffect } from "react";
33
44import ChatView from "../components/ChatView";
5import { parseDiffRouteSearch, stripDiffSearchParams } from "../diffRouteSearch";
6import { useMediaQuery } from "../hooks/useMediaQuery";
57import { useStore } from "../store";
8import { Sheet, SheetPopup } from "../components/ui/sheet";
9
10const DiffPanel = lazy(() => import("../components/DiffPanel"));
11const DIFF_INLINE_LAYOUT_MEDIA_QUERY = "(max-width: 1180px)";
12
13const DiffPanelWrapper = (props: {
14 children: ReactNode;
15 sheet: boolean;
16 diffOpen: boolean;
17 onCloseDiff: () => void;
18}) => {
19 if (props.sheet) {
20 return (
21 <Sheet
22 open={props.diffOpen}
23 onOpenChange={(open) => {
24 if (!open) {
25 props.onCloseDiff();
26 }
27 }}
28 >
29 <SheetPopup
30 side="right"
31 showCloseButton={false}
32 keepMounted
33 className="w-[min(88vw,820px)] max-w-[820px] p-0"
34 >
35 {props.children}
36 </SheetPopup>
37 </Sheet>
38 );
39 }
40
41 return (
42 <aside className={props.diffOpen ? undefined : "hidden"} aria-hidden={!props.diffOpen}>
43 {props.children}
44 </aside>
45 );
46};
647
748function ChatThreadRouteView() {
849 const { state } = useStore();
950 const navigate = useNavigate();
1051 const { threadId } = Route.useParams();
52 const search = Route.useSearch();
1153 const threadExists = threadId ? state.threads.some((thread) => thread.id === threadId) : false;
54 const diffOpen = search.diff === "1";
55 const shouldUseDiffSheet = useMediaQuery(DIFF_INLINE_LAYOUT_MEDIA_QUERY);
56 const closeDiff = useCallback(() => {
57 void navigate({
58 to: "/$threadId",
59 params: { threadId },
60 search: (previous) => {
61 return stripDiffSearchParams(previous);
62 },
63 });
64 }, [navigate, threadId]);
1265
1366 useEffect(() => {
1467 if (!threadId) {
......@@ -30,9 +83,32 @@ function ChatThreadRouteView() {
3083 return null;
3184 }
3285
33 return <ChatView threadId={threadId} />;
86 const diffLoadingFallback =
87 !diffOpen || shouldUseDiffSheet ? (
88 <div className="flex h-full min-h-0 items-center justify-center px-4 text-center text-xs text-muted-foreground/70">
89 Loading diff viewer...
90 </div>
91 ) : (
92 <aside className="flex h-full w-[560px] shrink-0 items-center justify-center border-l border-border bg-card px-4 text-center text-xs text-muted-foreground/70">
93 Loading diff viewer...
94 </aside>
95 );
96
97 return (
98 <>
99 <ChatView threadId={threadId} />
100 <Activity mode={diffOpen ? "visible" : "hidden"}>
101 <DiffPanelWrapper sheet={shouldUseDiffSheet} diffOpen={diffOpen} onCloseDiff={closeDiff}>
102 <Suspense fallback={diffLoadingFallback}>
103 <DiffPanel mode={shouldUseDiffSheet ? "sheet" : "inline"} />
104 </Suspense>
105 </DiffPanelWrapper>
106 </Activity>
107 </>
108 );
34109}
35110
36111export const Route = createFileRoute("/_chat/$threadId")({
112 validateSearch: (search) => parseDiffRouteSearch(search),
37113 component: ChatThreadRouteView,
38114});
apps/web/src/routes/_chat.tsx+6-66
......@@ -1,53 +1,10 @@
11import { Outlet, createFileRoute, useNavigate } from "@tanstack/react-router";
2import { Activity, Suspense, lazy, type ReactNode, useEffect } from "react";
2import { useEffect } from "react";
33
4import { DiffWorkerPoolProvider } from "../components/DiffWorkerPoolProvider";
45import Sidebar from "../components/Sidebar";
5import { useMediaQuery } from "../hooks/useMediaQuery";
6import { useStore } from "../store";
7import { Sheet, SheetPopup } from "../components/ui/sheet";
8
9const DiffPanel = lazy(() => import("../components/DiffPanel"));
10const DiffWorkerPoolProvider = lazy(() =>
11 import("../components/DiffPanel").then((module) => ({
12 default: module.DiffWorkerPoolProvider,
13 })),
14);
15const DIFF_INLINE_LAYOUT_MEDIA_QUERY = "(max-width: 1180px)";
16
17const DiffPanelWrapper = (props: { children: ReactNode; sheet: boolean }) => {
18 const { state, dispatch } = useStore();
19 if (props.sheet) {
20 return (
21 <Sheet
22 open={state.diffOpen}
23 onOpenChange={(open) => {
24 if (!open) {
25 dispatch({ type: "CLOSE_DIFF" });
26 }
27 }}
28 >
29 <SheetPopup
30 side="right"
31 showCloseButton={false}
32 keepMounted
33 className="w-[min(88vw,820px)] max-w-[820px] p-0"
34 >
35 {props.children}
36 </SheetPopup>
37 </Sheet>
38 );
39 }
40
41 return (
42 <aside className={state.diffOpen ? undefined : "hidden"} aria-hidden={!state.diffOpen}>
43 {props.children}
44 </aside>
45 );
46};
476
487function ChatRouteLayout() {
49 const { state } = useStore();
50 const shouldUseDiffSheet = useMediaQuery(DIFF_INLINE_LAYOUT_MEDIA_QUERY);
518 const navigate = useNavigate();
529
5310 useEffect(() => {
......@@ -66,30 +23,12 @@ function ChatRouteLayout() {
6623 };
6724 }, [navigate]);
6825
69 const diffLoadingFallback =
70 !state.diffOpen || shouldUseDiffSheet ? (
71 <div className="flex h-full min-h-0 items-center justify-center px-4 text-center text-xs text-muted-foreground/70">
72 Loading diff viewer...
73 </div>
74 ) : (
75 <aside className="flex h-full w-[560px] shrink-0 items-center justify-center border-l border-border bg-card px-4 text-center text-xs text-muted-foreground/70">
76 Loading diff viewer...
77 </aside>
78 );
79
8026 return (
8127 <div className="flex h-screen overflow-hidden bg-background text-foreground isolate">
8228 <Sidebar />
83 <Outlet />
84 <Activity mode={state.diffOpen ? "visible" : "hidden"}>
85 <DiffPanelWrapper sheet={shouldUseDiffSheet}>
86 <Suspense fallback={diffLoadingFallback}>
87 <DiffWorkerPoolProvider>
88 <DiffPanel mode={shouldUseDiffSheet ? "sheet" : "inline"} />
89 </DiffWorkerPoolProvider>
90 </Suspense>
91 </DiffPanelWrapper>
92 </Activity>
29 <DiffWorkerPoolProvider>
30 <Outlet />
31 </DiffWorkerPoolProvider>
9332 </div>
9433 );
9534}
......@@ -97,3 +36,4 @@ function ChatRouteLayout() {
9736export const Route = createFileRoute("/_chat")({
9837 component: ChatRouteLayout,
9938});
39
apps/web/src/store.test.ts-39
......@@ -81,10 +81,6 @@ function makeState(thread: Thread): AppState {
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", () => {
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", () => {
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", () => {
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", () => {
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", () => {
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", () => {
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", () => {
apps/web/src/store.ts+2-85
......@@ -48,20 +48,6 @@ type Action =
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 {
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
119101const PERSISTED_STATE_KEY = "t3code:renderer-state:v7";
......@@ -133,10 +115,6 @@ const initialState: AppState = {
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 {
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(
191165 return threads.map((t) => (t.id === threadId ? updater(t) : t));
192166}
193167
194function 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
220168function mergeTurnDiffSummaries(
221169 existing: Thread["turnDiffSummaries"],
222170 next: Thread["turnDiffSummaries"],
......@@ -615,13 +563,11 @@ export function reducer(state: AppState, action: Action): AppState {
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 {
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 {
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;
......@@ -1093,15 +1014,11 @@ export function reducer(state: AppState, action: Action): AppState {
10931014 runtimeMode: action.mode,
10941015 };
10951016
1096 case "DELETE_THREAD": {
1097 const threads = state.threads.filter((t) => t.id !== action.threadId);
1098 const diffState = resetDiffTargetIfMissing(state, threads);
1017 case "DELETE_THREAD":
10991018 return {
11001019 ...state,
1101 threads,
1102 ...diffState,
1020 threads: state.threads.filter((t) => t.id !== action.threadId),
11031021 };
1104 }
11051022
11061023 default:
11071024 return state;