authorgravatar for 97427849+flamboh@users.noreply.github.comoliver <97427849+flamboh@users.noreply.github.com> 2026-10-11 16:21:19-07:00
committergravatar for noreply@github.comGitHub <noreply@github.com> 2026-10-11 20:21:19-03:00
logf6fa4776e7d748b0af3fdf43bf3af095522a5bd3
tree596eaa1cdae3b55444b6fb3cb753afa29766c4ae
parentc9181d81a8f026f39679ef2363c976d581268b35
signature Signed by PGP key B5690EEEBB952194

perf(web): panel toggles no longer re-render the timeline and composer (#18077)


2 files changed, 60 insertions(+), 36 deletions(-)

apps/web/src/components/ChatView.tsx+54-30
...@@ -10913,6 +10913,45 @@ export default function ChatView(props: ChatViewProps) {...@@ -10913,6 +10913,45 @@ export default function ChatView(props: ChatViewProps) {
10913 }, [cancelWorktreeSetup, draftId, setupTarget.environmentId, worktreeSetup]);10913 }, [cancelWorktreeSetup, draftId, setupTarget.environmentId, worktreeSetup]);
10914 const onSendRef = useRef(onSend);10914 const onSendRef = useRef(onSend);
10915 onSendRef.current = onSend;10915 onSendRef.current = onSend;
10916 const onResumeRef = useRef(onResume);
10917 onResumeRef.current = onResume;
10918 const onCompactContextRef = useRef(onCompactContext);
10919 onCompactContextRef.current = onCompactContext;
10920 const onComposerSend = useCallback(
10921 (...args: Parameters<typeof onSend>) => onSendRef.current(...args),
10922 [],
10923 );
10924 const onComposerResume = useCallback(() => onResumeRef.current(), []);
10925 const onComposerCompactContext = useCallback(() => onCompactContextRef.current(), []);
10926 const queuedRunsControl = useMemo(
10927 () =>
10928 isServerThread && activeThread ? (
10929 <QueuedRunsControl
10930 ref={queuedRunsControlRef}
10931 steerShortcutLabel={shortcutLabelForCommand(keybindings, "thread.steerQueuedMessage", {
10932 context: { terminalFocus: false },
10933 })}
10934 editShortcutLabel={shortcutLabelForCommand(keybindings, "thread.editQueuedMessage", {
10935 context: { composerFocus: true },
10936 })}
10937 environmentId={activeThread.environmentId}
10938 threadId={activeThread.id}
10939 optimisticMessages={optimisticUserMessages}
10940 editingRunId={editingQueuedRun?.runId ?? null}
10941 onEditQueuedRun={beginEditingQueuedRun}
10942 onCancelEdit={cancelEditingQueuedRun}
10943 />
10944 ) : null,
10945 [
10946 activeThread,
10947 beginEditingQueuedRun,
10948 cancelEditingQueuedRun,
10949 editingQueuedRun?.runId,
10950 isServerThread,
10951 keybindings,
10952 optimisticUserMessages,
10953 ],
10954 );
10916 // Resend once the cancelled dispatch has settled and the composer is free.10955 // Resend once the cancelled dispatch has settled and the composer is free.
10917 // Every state that makes `onSend` bail and wait is part of the readiness10956 // Every state that makes `onSend` bail and wait is part of the readiness
10918 // check, so the flag survives a reconnect, a reverting checkpoint, or a10957 // check, so the flag survives a reconnect, a reverting checkpoint, or a
...@@ -10977,12 +11016,21 @@ export default function ChatView(props: ChatViewProps) {...@@ -10977,12 +11016,21 @@ export default function ChatView(props: ChatViewProps) {
10977 const onOpenTurnDiff = useCallback(11016 const onOpenTurnDiff = useCallback(
10978 (runId: RunId, filePath?: string) => {11017 (runId: RunId, filePath?: string) => {
10979 if (!isServerThread || !activeThreadRef) return;11018 if (!isServerThread || !activeThreadRef) return;
11019 const diffOpen =
11020 selectActiveRightPanel(useRightPanelStore.getState().byThreadKey, activeThreadRef) ===
11021 "diff";
10980 explicitDiffOpenRef.current = diffOpen ? null : activeThreadRef;11022 explicitDiffOpenRef.current = diffOpen ? null : activeThreadRef;
10981 useDiffPanelStore.getState().selectTurn(activeThreadRef, runId, filePath);11023 useDiffPanelStore.getState().selectTurn(activeThreadRef, runId, filePath);
10982 useRightPanelStore.getState().open(activeThreadRef, "diff");11024 useRightPanelStore.getState().open(activeThreadRef, "diff");
10983 onDiffPanelOpen?.();11025 onDiffPanelOpen?.();
10984 },11026 },
10985 [activeThreadRef, diffOpen, isServerThread, onDiffPanelOpen],11027 [activeThreadRef, isServerThread, onDiffPanelOpen],
11028 );
11029 const onTimelineRollbackCheckpoint = useCallback(
11030 (input: { readonly checkpointId: string; readonly scopeId: string }) => {
11031 if (!paintOnlyDisplayedTimeline) void onRollbackCheckpoint(input);
11032 },
11033 [onRollbackCheckpoint, paintOnlyDisplayedTimeline],
10986 );11034 );
10987 // The revert handler is read from a ref at call-time so the callback11035 // The revert handler is read from a ref at call-time so the callback
10988 // reference is fully stable and never busts TimelineRowCtx identity.11036 // reference is fully stable and never busts TimelineRowCtx identity.
...@@ -11504,9 +11552,7 @@ export default function ChatView(props: ChatViewProps) {...@@ -11504,9 +11552,7 @@ export default function ChatView(props: ChatViewProps) {
11504 onOpenThread={onOpenRelatedThread}11552 onOpenThread={onOpenRelatedThread}
11505 parentThreadLink={paintOnlyDisplayedTimeline ? null : parentThreadLink}11553 parentThreadLink={paintOnlyDisplayedTimeline ? null : parentThreadLink}
11506 onForkFromRun={paintOnlyDisplayedTimeline ? async () => {} : onForkFromRun}11554 onForkFromRun={paintOnlyDisplayedTimeline ? async () => {} : onForkFromRun}
11507 onRollbackCheckpoint={(input) => {11555 onRollbackCheckpoint={onTimelineRollbackCheckpoint}
11508 if (!paintOnlyDisplayedTimeline) void onRollbackCheckpoint(input);
11509 }}
11510 supportsConversationRollback={11556 supportsConversationRollback={
11511 !paintOnlyDisplayedTimeline && supportsConversationRollback11557 !paintOnlyDisplayedTimeline && supportsConversationRollback
11512 }11558 }
...@@ -11711,29 +11757,7 @@ export default function ChatView(props: ChatViewProps) {...@@ -11711,29 +11757,7 @@ export default function ChatView(props: ChatViewProps) {
11711 : projectCloneSendBlockReason11757 : projectCloneSendBlockReason
11712 }11758 }
11713 isPreparingWorktree={isPreparingWorktree && !sendQueuesBehindSetup}11759 isPreparingWorktree={isPreparingWorktree && !sendQueuesBehindSetup}
11714 queuedRunsControl={11760 queuedRunsControl={queuedRunsControl}
11715 isServerThread && activeThread ? (
11716 <QueuedRunsControl
11717 ref={queuedRunsControlRef}
11718 steerShortcutLabel={shortcutLabelForCommand(
11719 keybindings,
11720 "thread.steerQueuedMessage",
11721 { context: { terminalFocus: false } },
11722 )}
11723 editShortcutLabel={shortcutLabelForCommand(
11724 keybindings,
11725 "thread.editQueuedMessage",
11726 { context: { composerFocus: true } },
11727 )}
11728 environmentId={activeThread.environmentId}
11729 threadId={activeThread.id}
11730 optimisticMessages={optimisticUserMessages}
11731 editingRunId={editingQueuedRun?.runId ?? null}
11732 onEditQueuedRun={beginEditingQueuedRun}
11733 onCancelEdit={cancelEditingQueuedRun}
11734 />
11735 ) : null
11736 }
11737 bannerItems={composerBannerItems}11761 bannerItems={composerBannerItems}
11738 resumeCompactionTokens={resumeCompactionTokens}11762 resumeCompactionTokens={resumeCompactionTokens}
11739 keepFullHistory={keepFullHistory}11763 keepFullHistory={keepFullHistory}
...@@ -11807,9 +11831,9 @@ export default function ChatView(props: ChatViewProps) {...@@ -11807,9 +11831,9 @@ export default function ChatView(props: ChatViewProps) {
11807 onPageScrollKeyDown={onComposerPageScrollKeyDown}11831 onPageScrollKeyDown={onComposerPageScrollKeyDown}
11808 onPageScrollKeyUp={onComposerPageScrollKeyUp}11832 onPageScrollKeyUp={onComposerPageScrollKeyUp}
11809 onPageScrollRelease={onComposerPageScrollRelease}11833 onPageScrollRelease={onComposerPageScrollRelease}
11810 onCompactContext={onCompactContext}11834 onCompactContext={onComposerCompactContext}
11811 onSend={onSend}11835 onSend={onComposerSend}
11812 onResume={onResume}11836 onResume={onComposerResume}
11813 onInterrupt={onInterrupt}11837 onInterrupt={onInterrupt}
11814 onImplementPlanInNewThread={onImplementPlanInNewThread}11838 onImplementPlanInNewThread={onImplementPlanInNewThread}
11815 onRespondToApproval={onRespondToApproval}11839 onRespondToApproval={onRespondToApproval}
apps/web/src/components/chat/ThreadDetailsPanel.tsx+6-6
...@@ -10,18 +10,20 @@ import type { DraftId } from "../../composerDraftStore";...@@ -10,18 +10,20 @@ import type { DraftId } from "../../composerDraftStore";
10import { useT3ProjectFileScripts } from "../../hooks/useT3ProjectFileScripts";10import { useT3ProjectFileScripts } from "../../hooks/useT3ProjectFileScripts";
11import { type EnvMode, type EnvironmentOption } from "../BranchToolbar.logic";11import { type EnvMode, type EnvironmentOption } from "../BranchToolbar.logic";
12import { BranchToolbar } from "../BranchToolbar";12import { BranchToolbar } from "../BranchToolbar";
13import GitActionsControl from "../GitActionsControl";13import BaseGitActionsControl from "../GitActionsControl";
14import ProjectScriptsControl, {14import ProjectScriptsControl, {
15 type NewProjectScriptInput,15 type NewProjectScriptInput,
16 type ProjectScriptActionResult,16 type ProjectScriptActionResult,
17} from "../ProjectScriptsControl";17} from "../ProjectScriptsControl";
18import type { ComponentProps } from "react";18import { memo, type ComponentProps } from "react";
19import { ThreadDetailsCard } from "./ThreadDetailsCard";19import { ThreadDetailsCard } from "./ThreadDetailsCard";
20import { OpenInPicker } from "./OpenInPicker";20import { OpenInPicker } from "./OpenInPicker";
21import { ThreadDetailsSection } from "./ThreadDetailsSection";21import { ThreadDetailsSection } from "./ThreadDetailsSection";
22import { ThreadAutomationsPanel } from "./ThreadAutomationsPanel";22import { ThreadAutomationsPanel } from "./ThreadAutomationsPanel";
23import { ThreadRelationshipsPanel } from "./ThreadRelationshipsControl";23import { ThreadRelationshipsPanel } from "./ThreadRelationshipsControl";
2424
25const GitActionsControl = memo(BaseGitActionsControl);
26
25export interface ThreadDetailsPanelProps extends Pick<27export interface ThreadDetailsPanelProps extends Pick<
26 ComponentProps<typeof ThreadDetailsCard>,28 ComponentProps<typeof ThreadDetailsCard>,
27 "anchor" | "handle" | "onPresentationChange"29 "anchor" | "handle" | "onPresentationChange"
...@@ -67,6 +69,7 @@ export function ThreadDetailsPanel(props: ThreadDetailsPanelProps) {...@@ -67,6 +69,7 @@ export function ThreadDetailsPanel(props: ThreadDetailsPanelProps) {
67 props.environmentId,69 props.environmentId,
68 props.activeProjectScripts ? props.gitCwd : null,70 props.activeProjectScripts ? props.gitCwd : null,
69 );71 );
72 const activeThreadRef = { environmentId: props.environmentId, threadId: props.threadId };
70 const branchToolbarProps = {73 const branchToolbarProps = {
71 showGitControls: props.isGitRepo,74 showGitControls: props.isGitRepo,
72 environmentId: props.environmentId,75 environmentId: props.environmentId,
...@@ -160,10 +163,7 @@ export function ThreadDetailsPanel(props: ThreadDetailsPanelProps) {...@@ -160,10 +163,7 @@ export function ThreadDetailsPanel(props: ThreadDetailsPanelProps) {
160 displayMode="panel"163 displayMode="panel"
161 compact={density !== "full"}164 compact={density !== "full"}
162 gitCwd={props.gitCwd}165 gitCwd={props.gitCwd}
163 activeThreadRef={{166 activeThreadRef={activeThreadRef}
164 environmentId: props.environmentId,
165 threadId: props.threadId,
166 }}
167 {...(props.draftId ? { draftId: props.draftId } : {})}167 {...(props.draftId ? { draftId: props.draftId } : {})}
168 {...(props.onOpenChanges ? { onOpenChanges: props.onOpenChanges } : {})}168 {...(props.onOpenChanges ? { onOpenChanges: props.onOpenChanges } : {})}
169 />169 />