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) {
1091310913 }, [cancelWorktreeSetup, draftId, setupTarget.environmentId, worktreeSetup]);
1091410914 const onSendRef = useRef(onSend);
1091510915 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 );
1091610955 // Resend once the cancelled dispatch has settled and the composer is free.
1091710956 // Every state that makes `onSend` bail and wait is part of the readiness
1091810957 // check, so the flag survives a reconnect, a reverting checkpoint, or a
......@@ -10977,12 +11016,21 @@ export default function ChatView(props: ChatViewProps) {
1097711016 const onOpenTurnDiff = useCallback(
1097811017 (runId: RunId, filePath?: string) => {
1097911018 if (!isServerThread || !activeThreadRef) return;
11019 const diffOpen =
11020 selectActiveRightPanel(useRightPanelStore.getState().byThreadKey, activeThreadRef) ===
11021 "diff";
1098011022 explicitDiffOpenRef.current = diffOpen ? null : activeThreadRef;
1098111023 useDiffPanelStore.getState().selectTurn(activeThreadRef, runId, filePath);
1098211024 useRightPanelStore.getState().open(activeThreadRef, "diff");
1098311025 onDiffPanelOpen?.();
1098411026 },
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],
1098611034 );
1098711035 // The revert handler is read from a ref at call-time so the callback
1098811036 // reference is fully stable and never busts TimelineRowCtx identity.
......@@ -11504,9 +11552,7 @@ export default function ChatView(props: ChatViewProps) {
1150411552 onOpenThread={onOpenRelatedThread}
1150511553 parentThreadLink={paintOnlyDisplayedTimeline ? null : parentThreadLink}
1150611554 onForkFromRun={paintOnlyDisplayedTimeline ? async () => {} : onForkFromRun}
11507 onRollbackCheckpoint={(input) => {
11508 if (!paintOnlyDisplayedTimeline) void onRollbackCheckpoint(input);
11509 }}
11555 onRollbackCheckpoint={onTimelineRollbackCheckpoint}
1151011556 supportsConversationRollback={
1151111557 !paintOnlyDisplayedTimeline && supportsConversationRollback
1151211558 }
......@@ -11711,29 +11757,7 @@ export default function ChatView(props: ChatViewProps) {
1171111757 : projectCloneSendBlockReason
1171211758 }
1171311759 isPreparingWorktree={isPreparingWorktree && !sendQueuesBehindSetup}
11714 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 }
11760 queuedRunsControl={queuedRunsControl}
1173711761 bannerItems={composerBannerItems}
1173811762 resumeCompactionTokens={resumeCompactionTokens}
1173911763 keepFullHistory={keepFullHistory}
......@@ -11807,9 +11831,9 @@ export default function ChatView(props: ChatViewProps) {
1180711831 onPageScrollKeyDown={onComposerPageScrollKeyDown}
1180811832 onPageScrollKeyUp={onComposerPageScrollKeyUp}
1180911833 onPageScrollRelease={onComposerPageScrollRelease}
11810 onCompactContext={onCompactContext}
11811 onSend={onSend}
11812 onResume={onResume}
11834 onCompactContext={onComposerCompactContext}
11835 onSend={onComposerSend}
11836 onResume={onComposerResume}
1181311837 onInterrupt={onInterrupt}
1181411838 onImplementPlanInNewThread={onImplementPlanInNewThread}
1181511839 onRespondToApproval={onRespondToApproval}
apps/web/src/components/chat/ThreadDetailsPanel.tsx+6-6
......@@ -10,18 +10,20 @@ import type { DraftId } from "../../composerDraftStore";
1010import { useT3ProjectFileScripts } from "../../hooks/useT3ProjectFileScripts";
1111import { type EnvMode, type EnvironmentOption } from "../BranchToolbar.logic";
1212import { BranchToolbar } from "../BranchToolbar";
13import GitActionsControl from "../GitActionsControl";
13import BaseGitActionsControl from "../GitActionsControl";
1414import ProjectScriptsControl, {
1515 type NewProjectScriptInput,
1616 type ProjectScriptActionResult,
1717} from "../ProjectScriptsControl";
18import type { ComponentProps } from "react";
18import { memo, type ComponentProps } from "react";
1919import { ThreadDetailsCard } from "./ThreadDetailsCard";
2020import { OpenInPicker } from "./OpenInPicker";
2121import { ThreadDetailsSection } from "./ThreadDetailsSection";
2222import { ThreadAutomationsPanel } from "./ThreadAutomationsPanel";
2323import { ThreadRelationshipsPanel } from "./ThreadRelationshipsControl";
2424
25const GitActionsControl = memo(BaseGitActionsControl);
26
2527export interface ThreadDetailsPanelProps extends Pick<
2628 ComponentProps<typeof ThreadDetailsCard>,
2729 "anchor" | "handle" | "onPresentationChange"
......@@ -67,6 +69,7 @@ export function ThreadDetailsPanel(props: ThreadDetailsPanelProps) {
6769 props.environmentId,
6870 props.activeProjectScripts ? props.gitCwd : null,
6971 );
72 const activeThreadRef = { environmentId: props.environmentId, threadId: props.threadId };
7073 const branchToolbarProps = {
7174 showGitControls: props.isGitRepo,
7275 environmentId: props.environmentId,
......@@ -160,10 +163,7 @@ export function ThreadDetailsPanel(props: ThreadDetailsPanelProps) {
160163 displayMode="panel"
161164 compact={density !== "full"}
162165 gitCwd={props.gitCwd}
163 activeThreadRef={{
164 environmentId: props.environmentId,
165 threadId: props.threadId,
166 }}
166 activeThreadRef={activeThreadRef}
167167 {...(props.draftId ? { draftId: props.draftId } : {})}
168168 {...(props.onOpenChanges ? { onOpenChanges: props.onOpenChanges } : {})}
169169 />