authorgravatar for julius0216@outlook.comJulius Marminge <julius0216@outlook.com> 2026-02-19 09:49:50-08:00
committergravatar for julius0216@outlook.comJulius Marminge <julius0216@outlook.com> 2026-02-19 09:49:50-08:00
log891eb02d49d4b511cc1b52ed4d675476484f2aa9
tree79751a7d5c7f11c53c2f13bdf18ba6e19b61030e
parentcc3e1db22a0a450396bf34650eebb7b8c56129de

Move diff panel into thread route and lift worker pool provider

- Extract `DiffWorkerPoolProvider` into its own component and wrap `_chat` outlet with it - Render/lazy-load `DiffPanel` from `/_chat/$threadId` so diff UI state and close behavior live with thread routing - Keep responsive sheet vs inline diff presentation with route-level fallback loading UI

4 files changed, 117 insertions(+), 128 deletions(-)

apps/web/src/components/DiffPanel.tsx+2-24
......@@ -3,12 +3,11 @@ import {
33 FileDiff,
44 type FileDiffMetadata,
55 Virtualizer,
6 WorkerPoolContextProvider,
76} from "@pierre/diffs/react";
87import { useQuery } from "@tanstack/react-query";
98import { useNavigate, useParams, useSearch } from "@tanstack/react-router";
109import { Columns2Icon, Rows3Icon } from "lucide-react";
11import { type ReactNode, useEffect, useMemo, useRef, useState } from "react";
10import { useEffect, useMemo, useRef, useState } from "react";
1211import { checkpointDiffQueryOptions } from "~/lib/providerReactQuery";
1312import { cn } from "~/lib/utils";
1413import { parseDiffRouteSearch } from "../diffRouteSearch";
......@@ -77,28 +76,7 @@ interface DiffPanelProps {
7776 mode?: "inline" | "sheet";
7877}
7978
80export function DiffWorkerPoolProvider({ children }: { children?: ReactNode }) {
81 const workerPoolSize = useMemo(() => {
82 const cores = typeof navigator === "undefined" ? 4 : Math.max(1, navigator.hardwareConcurrency || 4);
83 return Math.max(2, Math.min(6, Math.floor(cores / 2)));
84 }, []);
85
86 return (
87 <WorkerPoolContextProvider
88 poolOptions={{
89 workerFactory: () =>
90 new Worker(new URL("../workers/diffs.worker.ts", import.meta.url), { type: "module" }),
91 poolSize: workerPoolSize,
92 totalASTLRUCacheSize: 240,
93 }}
94 highlighterOptions={{
95 tokenizeMaxLineLength: 1_000,
96 }}
97 >
98 {children}
99 </WorkerPoolContextProvider>
100 );
101}
79export { DiffWorkerPoolProvider } from "./DiffWorkerPoolProvider";
10280
10381export default function DiffPanel({ mode = "inline" }: DiffPanelProps) {
10482 const api = useNativeApi();
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/routes/_chat.$threadId.tsx+83-2
......@@ -1,15 +1,73 @@
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";
55import { parseDiffRouteSearch } from "../diffRouteSearch";
6import { useMediaQuery } from "../hooks/useMediaQuery";
67import { 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};
747
848function ChatThreadRouteView() {
949 const { state } = useStore();
1050 const navigate = useNavigate();
1151 const { threadId } = Route.useParams();
52 const search = Route.useSearch();
1253 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 const {
62 diff: _diff,
63 diffTurnId: _diffTurnId,
64 diffFilePath: _diffFilePath,
65 ...rest
66 } = previous;
67 return rest;
68 },
69 });
70 }, [navigate, threadId]);
1371
1472 useEffect(() => {
1573 if (!threadId) {
......@@ -31,10 +89,33 @@ function ChatThreadRouteView() {
3189 return null;
3290 }
3391
34 return <ChatView threadId={threadId} />;
92 const diffLoadingFallback =
93 !diffOpen || shouldUseDiffSheet ? (
94 <div className="flex h-full min-h-0 items-center justify-center px-4 text-center text-xs text-muted-foreground/70">
95 Loading diff viewer...
96 </div>
97 ) : (
98 <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">
99 Loading diff viewer...
100 </aside>
101 );
102
103 return (
104 <>
105 <ChatView threadId={threadId} />
106 <Activity mode={diffOpen ? "visible" : "hidden"}>
107 <DiffPanelWrapper sheet={shouldUseDiffSheet} diffOpen={diffOpen} onCloseDiff={closeDiff}>
108 <Suspense fallback={diffLoadingFallback}>
109 <DiffPanel mode={shouldUseDiffSheet ? "sheet" : "inline"} />
110 </Suspense>
111 </DiffPanelWrapper>
112 </Activity>
113 </>
114 );
35115}
36116
37117export const Route = createFileRoute("/_chat/$threadId")({
38118 validateSearch: (search) => parseDiffRouteSearch(search),
39119 component: ChatThreadRouteView,
40120});
121
apps/web/src/routes/_chat.tsx+6-102
......@@ -1,112 +1,15 @@
1import {
2 Outlet,
3 createFileRoute,
4 useNavigate,
5 useParams,
6 useSearch,
7} from "@tanstack/react-router";
8import { Activity, Suspense, lazy, type ReactNode, useCallback, useMemo } from "react";
1import { Outlet, createFileRoute } from "@tanstack/react-router";
92
3import { DiffWorkerPoolProvider } from "../components/DiffWorkerPoolProvider";
104import Sidebar from "../components/Sidebar";
11import { parseDiffRouteSearch } from "../diffRouteSearch";
12import { useMediaQuery } from "../hooks/useMediaQuery";
13import { Sheet, SheetPopup } from "../components/ui/sheet";
14
15const DiffPanel = lazy(() => import("../components/DiffPanel"));
16const DiffWorkerPoolProvider = lazy(() =>
17 import("../components/DiffPanel").then((module) => ({
18 default: module.DiffWorkerPoolProvider,
19 })),
20);
21const DIFF_INLINE_LAYOUT_MEDIA_QUERY = "(max-width: 1180px)";
22
23const DiffPanelWrapper = (props: {
24 children: ReactNode;
25 sheet: boolean;
26 diffOpen: boolean;
27 onCloseDiff: () => void;
28}) => {
29 if (props.sheet) {
30 return (
31 <Sheet
32 open={props.diffOpen}
33 onOpenChange={(open) => {
34 if (!open) {
35 props.onCloseDiff();
36 }
37 }}
38 >
39 <SheetPopup
40 side="right"
41 showCloseButton={false}
42 keepMounted
43 className="w-[min(88vw,820px)] max-w-[820px] p-0"
44 >
45 {props.children}
46 </SheetPopup>
47 </Sheet>
48 );
49 }
50
51 return (
52 <aside className={props.diffOpen ? undefined : "hidden"} aria-hidden={!props.diffOpen}>
53 {props.children}
54 </aside>
55 );
56};
575
586function ChatRouteLayout() {
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";
68 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]);
85
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
977 return (
988 <div className="flex h-screen overflow-hidden bg-background text-foreground isolate">
999 <Sidebar />
100 <Outlet />
101 <Activity mode={diffOpen ? "visible" : "hidden"}>
102 <DiffPanelWrapper sheet={shouldUseDiffSheet} diffOpen={diffOpen} onCloseDiff={closeDiff}>
103 <Suspense fallback={diffLoadingFallback}>
104 <DiffWorkerPoolProvider>
105 <DiffPanel mode={shouldUseDiffSheet ? "sheet" : "inline"} />
106 </DiffWorkerPoolProvider>
107 </Suspense>
108 </DiffPanelWrapper>
109 </Activity>
10 <DiffWorkerPoolProvider>
11 <Outlet />
12 </DiffWorkerPoolProvider>
11013 </div>
11114 );
11215}
......@@ -114,3 +17,4 @@ function ChatRouteLayout() {
11417export const Route = createFileRoute("/_chat")({
11518 component: ChatRouteLayout,
11619});
20