From f6fa4776e7d748b0af3fdf43bf3af095522a5bd3 Mon Sep 17 00:00:00 2001 From: oliver <97427849+flamboh@users.noreply.github.com> Date: Sun, 11 Oct 2026 16:21:19 -0700 Subject: [PATCH] perf(web): panel toggles no longer re-render the timeline and composer (#18077) --- apps/web/src/components/ChatView.tsx | 84 ++++++++++++------- .../components/chat/ThreadDetailsPanel.tsx | 12 +-- 2 files changed, 60 insertions(+), 36 deletions(-) diff --git a/apps/web/src/components/ChatView.tsx b/apps/web/src/components/ChatView.tsx index 260db2e47b4f304a8c4fe3061016f353523475c7..4b6d03636192fb6ce80b365c42fae58e1e53b4c5 100644 --- a/apps/web/src/components/ChatView.tsx +++ b/apps/web/src/components/ChatView.tsx @@ -10913,6 +10913,45 @@ export default function ChatView(props: ChatViewProps) { }, [cancelWorktreeSetup, draftId, setupTarget.environmentId, worktreeSetup]); const onSendRef = useRef(onSend); onSendRef.current = onSend; + const onResumeRef = useRef(onResume); + onResumeRef.current = onResume; + const onCompactContextRef = useRef(onCompactContext); + onCompactContextRef.current = onCompactContext; + const onComposerSend = useCallback( + (...args: Parameters) => onSendRef.current(...args), + [], + ); + const onComposerResume = useCallback(() => onResumeRef.current(), []); + const onComposerCompactContext = useCallback(() => onCompactContextRef.current(), []); + const queuedRunsControl = useMemo( + () => + isServerThread && activeThread ? ( + + ) : null, + [ + activeThread, + beginEditingQueuedRun, + cancelEditingQueuedRun, + editingQueuedRun?.runId, + isServerThread, + keybindings, + optimisticUserMessages, + ], + ); // Resend once the cancelled dispatch has settled and the composer is free. // Every state that makes `onSend` bail and wait is part of the readiness // check, so the flag survives a reconnect, a reverting checkpoint, or a @@ -10977,12 +11016,21 @@ export default function ChatView(props: ChatViewProps) { const onOpenTurnDiff = useCallback( (runId: RunId, filePath?: string) => { if (!isServerThread || !activeThreadRef) return; + const diffOpen = + selectActiveRightPanel(useRightPanelStore.getState().byThreadKey, activeThreadRef) === + "diff"; explicitDiffOpenRef.current = diffOpen ? null : activeThreadRef; useDiffPanelStore.getState().selectTurn(activeThreadRef, runId, filePath); useRightPanelStore.getState().open(activeThreadRef, "diff"); onDiffPanelOpen?.(); }, - [activeThreadRef, diffOpen, isServerThread, onDiffPanelOpen], + [activeThreadRef, isServerThread, onDiffPanelOpen], + ); + const onTimelineRollbackCheckpoint = useCallback( + (input: { readonly checkpointId: string; readonly scopeId: string }) => { + if (!paintOnlyDisplayedTimeline) void onRollbackCheckpoint(input); + }, + [onRollbackCheckpoint, paintOnlyDisplayedTimeline], ); // The revert handler is read from a ref at call-time so the callback // reference is fully stable and never busts TimelineRowCtx identity. @@ -11504,9 +11552,7 @@ export default function ChatView(props: ChatViewProps) { onOpenThread={onOpenRelatedThread} parentThreadLink={paintOnlyDisplayedTimeline ? null : parentThreadLink} onForkFromRun={paintOnlyDisplayedTimeline ? async () => {} : onForkFromRun} - onRollbackCheckpoint={(input) => { - if (!paintOnlyDisplayedTimeline) void onRollbackCheckpoint(input); - }} + onRollbackCheckpoint={onTimelineRollbackCheckpoint} supportsConversationRollback={ !paintOnlyDisplayedTimeline && supportsConversationRollback } @@ -11711,29 +11757,7 @@ export default function ChatView(props: ChatViewProps) { : projectCloneSendBlockReason } isPreparingWorktree={isPreparingWorktree && !sendQueuesBehindSetup} - queuedRunsControl={ - isServerThread && activeThread ? ( - - ) : null - } + queuedRunsControl={queuedRunsControl} bannerItems={composerBannerItems} resumeCompactionTokens={resumeCompactionTokens} keepFullHistory={keepFullHistory} @@ -11807,9 +11831,9 @@ export default function ChatView(props: ChatViewProps) { onPageScrollKeyDown={onComposerPageScrollKeyDown} onPageScrollKeyUp={onComposerPageScrollKeyUp} onPageScrollRelease={onComposerPageScrollRelease} - onCompactContext={onCompactContext} - onSend={onSend} - onResume={onResume} + onCompactContext={onComposerCompactContext} + onSend={onComposerSend} + onResume={onComposerResume} onInterrupt={onInterrupt} onImplementPlanInNewThread={onImplementPlanInNewThread} onRespondToApproval={onRespondToApproval} diff --git a/apps/web/src/components/chat/ThreadDetailsPanel.tsx b/apps/web/src/components/chat/ThreadDetailsPanel.tsx index 4bfcaa78811873b3f3c5d4ac708594ccaa897fb8..d6686df77087587555e8d38dca776eb3facd3b16 100644 --- a/apps/web/src/components/chat/ThreadDetailsPanel.tsx +++ b/apps/web/src/components/chat/ThreadDetailsPanel.tsx @@ -10,18 +10,20 @@ import type { DraftId } from "../../composerDraftStore"; import { useT3ProjectFileScripts } from "../../hooks/useT3ProjectFileScripts"; import { type EnvMode, type EnvironmentOption } from "../BranchToolbar.logic"; import { BranchToolbar } from "../BranchToolbar"; -import GitActionsControl from "../GitActionsControl"; +import BaseGitActionsControl from "../GitActionsControl"; import ProjectScriptsControl, { type NewProjectScriptInput, type ProjectScriptActionResult, } from "../ProjectScriptsControl"; -import type { ComponentProps } from "react"; +import { memo, type ComponentProps } from "react"; import { ThreadDetailsCard } from "./ThreadDetailsCard"; import { OpenInPicker } from "./OpenInPicker"; import { ThreadDetailsSection } from "./ThreadDetailsSection"; import { ThreadAutomationsPanel } from "./ThreadAutomationsPanel"; import { ThreadRelationshipsPanel } from "./ThreadRelationshipsControl"; +const GitActionsControl = memo(BaseGitActionsControl); + export interface ThreadDetailsPanelProps extends Pick< ComponentProps, "anchor" | "handle" | "onPresentationChange" @@ -67,6 +69,7 @@ export function ThreadDetailsPanel(props: ThreadDetailsPanelProps) { props.environmentId, props.activeProjectScripts ? props.gitCwd : null, ); + const activeThreadRef = { environmentId: props.environmentId, threadId: props.threadId }; const branchToolbarProps = { showGitControls: props.isGitRepo, environmentId: props.environmentId, @@ -160,10 +163,7 @@ export function ThreadDetailsPanel(props: ThreadDetailsPanelProps) { displayMode="panel" compact={density !== "full"} gitCwd={props.gitCwd} - activeThreadRef={{ - environmentId: props.environmentId, - threadId: props.threadId, - }} + activeThreadRef={activeThreadRef} {...(props.draftId ? { draftId: props.draftId } : {})} {...(props.onOpenChanges ? { onOpenChanges: props.onOpenChanges } : {})} /> -- 2.54.0