Skip to content

Commit 8cbe456

Browse files
AfzalHclaude
andcommitted
Diff panel: per-thread state, full-width toggle, and per-file collapse
Move diff-panel UI state from URL search params and component-local useState into the Zustand UI store. State that is meaningful per thread is keyed by scopedThreadKey so two threads can have different diff panels open / expanded / collapsed independently. Wider state (render mode, line wrap) lives in the store globally so it survives the remount that expand/collapse causes. Per-thread (persisted to localStorage): - diff panel open/closed (replaces ?diff=1 in URL) - full-width vs split-pane (new toolbar toggle) - per-file collapse (new chevron on each file header) Global session (in store, not persisted to disk): - render mode (split/stacked) - line wrap, hydrated once from settings.diffWordWrap The per-file collapse swaps the FileDiff out for a small CollapsedFileHeader rather than hiding content via CSS — the library's Virtualizer reserves vertical space from hunk metadata, so a CSS hide leaves a tall empty box and triggers ResizeObserver thrashing during scroll. The collapsed header inlines the same change-type SVG paths the library renders inside its shadow DOM so the new/modified/deleted/renamed icon is identical between collapsed and expanded views. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
1 parent 17b4396 commit 8cbe456

7 files changed

Lines changed: 917 additions & 138 deletions

File tree

apps/web/src/components/ChatView.tsx

Lines changed: 22 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -36,14 +36,14 @@ import { projectScriptCwd, projectScriptRuntimeEnv } from "@t3tools/shared/proje
3636
import { truncate } from "@t3tools/shared/String";
3737
import { Debouncer } from "@tanstack/react-pacer";
3838
import { memo, useCallback, useEffect, useMemo, useRef, useState } from "react";
39-
import { useNavigate, useSearch } from "@tanstack/react-router";
39+
import { useNavigate } from "@tanstack/react-router";
4040
import { useShallow } from "zustand/react/shallow";
4141
import { useGitStatus } from "~/lib/gitStatusState";
4242
import { usePrimaryEnvironmentId } from "../environments/primary";
4343
import { readEnvironmentApi } from "../environmentApi";
4444
import { isElectron } from "../env";
4545
import { readLocalApi } from "../localApi";
46-
import { parseDiffRouteSearch, stripDiffSearchParams } from "../diffRouteSearch";
46+
import { stripDiffSearchParams } from "../diffRouteSearch";
4747
import {
4848
collapseExpandedComposerCursor,
4949
parseStandaloneComposerSlashCommand,
@@ -620,10 +620,6 @@ export default function ChatView(props: ChatViewProps) {
620620
const timestampFormat = settings.timestampFormat;
621621
const autoOpenPlanSidebar = settings.autoOpenPlanSidebar;
622622
const navigate = useNavigate();
623-
const rawSearch = useSearch({
624-
strict: false,
625-
select: (params) => parseDiffRouteSearch(params),
626-
});
627623
const { resolvedTheme } = useTheme();
628624
// Granular store selectors — avoid subscribing to prompt changes.
629625
const composerRuntimeMode = useComposerDraftStore(
@@ -794,13 +790,16 @@ export default function ChatView(props: ChatViewProps) {
794790
composerInteractionMode ?? activeThread?.interactionMode ?? DEFAULT_INTERACTION_MODE;
795791
const isLocalDraftThread = !isServerThread && localDraftThread !== undefined;
796792
const canCheckoutPullRequestIntoThread = isLocalDraftThread;
797-
const diffOpen = rawSearch.diff === "1";
798793
const activeThreadId = activeThread?.id ?? null;
799794
const activeThreadRef = useMemo(
800795
() => (activeThread ? scopeThreadRef(activeThread.environmentId, activeThread.id) : null),
801796
[activeThread],
802797
);
803798
const activeThreadKey = activeThreadRef ? scopedThreadKey(activeThreadRef) : null;
799+
const diffOpen = useUiStateStore((store) =>
800+
activeThreadKey ? store.threadDiffOpenById[activeThreadKey] === true : false,
801+
);
802+
const setThreadDiffOpen = useUiStateStore((store) => store.setThreadDiffOpen);
804803
const existingOpenTerminalThreadKeys = useMemo(() => {
805804
const existingThreadKeys = new Set<string>([...serverThreadKeys, ...draftThreadKeys]);
806805
return openTerminalThreadKeys.filter((nextThreadKey) => existingThreadKeys.has(nextThreadKey));
@@ -1498,25 +1497,14 @@ export default function ChatView(props: ChatViewProps) {
14981497
[keybindings, nonTerminalShortcutLabelOptions],
14991498
);
15001499
const onToggleDiff = useCallback(() => {
1501-
if (!isServerThread) {
1500+
if (!isServerThread || !activeThreadKey) {
15021501
return;
15031502
}
15041503
if (!diffOpen) {
15051504
onDiffPanelOpen?.();
15061505
}
1507-
void navigate({
1508-
to: "/$environmentId/$threadId",
1509-
params: {
1510-
environmentId,
1511-
threadId,
1512-
},
1513-
replace: true,
1514-
search: (previous) => {
1515-
const rest = stripDiffSearchParams(previous);
1516-
return diffOpen ? { ...rest, diff: undefined } : { ...rest, diff: "1" };
1517-
},
1518-
});
1519-
}, [diffOpen, environmentId, isServerThread, navigate, onDiffPanelOpen, threadId]);
1506+
setThreadDiffOpen(activeThreadKey, !diffOpen);
1507+
}, [activeThreadKey, diffOpen, isServerThread, onDiffPanelOpen, setThreadDiffOpen]);
15201508

15211509
const envLocked = Boolean(
15221510
activeThread &&
@@ -3237,10 +3225,11 @@ export default function ChatView(props: ChatViewProps) {
32373225
}, []);
32383226
const onOpenTurnDiff = useCallback(
32393227
(turnId: TurnId, filePath?: string) => {
3240-
if (!isServerThread) {
3228+
if (!isServerThread || !activeThreadKey) {
32413229
return;
32423230
}
32433231
onDiffPanelOpen?.();
3232+
setThreadDiffOpen(activeThreadKey, true);
32443233
void navigate({
32453234
to: "/$environmentId/$threadId",
32463235
params: {
@@ -3250,12 +3239,20 @@ export default function ChatView(props: ChatViewProps) {
32503239
search: (previous) => {
32513240
const rest = stripDiffSearchParams(previous);
32523241
return filePath
3253-
? { ...rest, diff: "1", diffTurnId: turnId, diffFilePath: filePath }
3254-
: { ...rest, diff: "1", diffTurnId: turnId };
3242+
? { ...rest, diffTurnId: turnId, diffFilePath: filePath }
3243+
: { ...rest, diffTurnId: turnId };
32553244
},
32563245
});
32573246
},
3258-
[environmentId, isServerThread, navigate, onDiffPanelOpen, threadId],
3247+
[
3248+
activeThreadKey,
3249+
environmentId,
3250+
isServerThread,
3251+
navigate,
3252+
onDiffPanelOpen,
3253+
setThreadDiffOpen,
3254+
threadId,
3255+
],
32593256
);
32603257
// Both the Map and the revert handler are read from refs at call-time so
32613258
// the callback reference is fully stable and never busts context identity.

0 commit comments

Comments
 (0)