diff --git a/apps/web/src/components/ChatView.tsx b/apps/web/src/components/ChatView.tsx index 99e0421b403..9621f5f1674 100644 --- a/apps/web/src/components/ChatView.tsx +++ b/apps/web/src/components/ChatView.tsx @@ -117,6 +117,11 @@ import { useTurnDiffSummaries } from "../hooks/useTurnDiffSummaries"; import { isCommandPaletteOpen } from "../commandPaletteBus"; import { buildTemporaryWorktreeBranchName } from "@t3tools/shared/git"; import { useMediaQuery } from "../hooks/useMediaQuery"; +import { + clearPlanSidebarDismissal, + dismissPlanSidebarForTurn, + isPlanSidebarDismissedForTurn, +} from "../planSidebarDismissal"; import { RIGHT_PANEL_INLINE_LAYOUT_MEDIA_QUERY } from "../rightPanelLayout"; import { selectActiveRightPanel, @@ -1310,8 +1315,6 @@ function ChatViewContent(props: ChatViewProps) { const [pendingUserInputQuestionIndexByRequestId, setPendingUserInputQuestionIndexByRequestId] = useState>({}); const shouldUsePlanSidebarSheet = useMediaQuery(RIGHT_PANEL_INLINE_LAYOUT_MEDIA_QUERY); - // Tracks whether the user explicitly dismissed the sidebar for the active turn. - const planSidebarDismissedForTurnRef = useRef(null); // When set, the thread-change reset effect will open the sidebar instead of closing it. // Used by "Implement in a new thread" to carry the sidebar-open intent across navigation. const planSidebarOpenOnNextThreadRef = useRef(false); @@ -3110,18 +3113,21 @@ function ChatViewContent(props: ChatViewProps) { handleInteractionModeChange(interactionMode === "plan" ? "default" : "plan"); }, [handleInteractionModeChange, interactionMode]); const dismissPlanSidebarForCurrentTurn = useCallback(() => { - planSidebarDismissedForTurnRef.current = - activePlan?.turnId ?? sidebarProposedPlan?.turnId ?? "__dismissed__"; - }, [activePlan?.turnId, sidebarProposedPlan?.turnId]); + if (!activeThreadKey) return; + dismissPlanSidebarForTurn( + activeThreadKey, + activePlan?.turnId ?? sidebarProposedPlan?.turnId ?? "__dismissed__", + ); + }, [activeThreadKey, activePlan?.turnId, sidebarProposedPlan?.turnId]); const togglePlanSidebar = useCallback(() => { if (!activeThreadRef) return; if (planSidebarOpen) { dismissPlanSidebarForCurrentTurn(); - } else { - planSidebarDismissedForTurnRef.current = null; + } else if (activeThreadKey) { + clearPlanSidebarDismissal(activeThreadKey); } useRightPanelStore.getState().toggle(activeThreadRef, "plan"); - }, [activeThreadRef, dismissPlanSidebarForCurrentTurn, planSidebarOpen]); + }, [activeThreadKey, activeThreadRef, dismissPlanSidebarForCurrentTurn, planSidebarOpen]); const closePlanSidebar = useCallback(() => { if (!activeThreadRef) return; setMaximizedRightPanelThreadKey(null); @@ -3283,7 +3289,7 @@ function ChatViewContent(props: ChatViewProps) { (surface: RightPanelSurface) => { if (!activeThreadRef) return; if (surface.kind === "plan") { - planSidebarDismissedForTurnRef.current = null; + clearPlanSidebarDismissal(scopedThreadKey(activeThreadRef)); } else if (planSidebarOpen) { dismissPlanSidebarForCurrentTurn(); } @@ -3852,10 +3858,10 @@ function ChatViewContent(props: ChatViewProps) { if (planSidebarOpenOnNextThreadRef.current) { planSidebarOpenOnNextThreadRef.current = false; if (activeThreadRef) { + clearPlanSidebarDismissal(scopedThreadKey(activeThreadRef)); useRightPanelStore.getState().open(activeThreadRef, "plan"); } } - planSidebarDismissedForTurnRef.current = null; // activeThreadRef resets transitively with the active thread. }, [activeThread?.id]); @@ -3868,10 +3874,9 @@ function ChatViewContent(props: ChatViewProps) { const latestTurnId = activeLatestTurn?.turnId ?? null; if (latestTurnId && activePlan.turnId !== latestTurnId) return; const turnKey = activePlan.turnId ?? sidebarProposedPlan?.turnId ?? "__dismissed__"; - if (planSidebarDismissedForTurnRef.current === turnKey) return; - if (activeThreadRef) { - useRightPanelStore.getState().open(activeThreadRef, "plan"); - } + if (!activeThreadRef) return; + if (isPlanSidebarDismissedForTurn(scopedThreadKey(activeThreadRef), turnKey)) return; + useRightPanelStore.getState().open(activeThreadRef, "plan"); }, [ activePlan, activeLatestTurn?.turnId, @@ -5412,8 +5417,8 @@ function ChatViewContent(props: ChatViewProps) { // "default" mode here means the agent is executing the plan, which produces // step-tracking activities that the sidebar will display. if (nextInteractionMode === "default" && autoOpenPlanSidebar) { - planSidebarDismissedForTurnRef.current = null; if (activeThreadRef) { + clearPlanSidebarDismissal(scopedThreadKey(activeThreadRef)); useRightPanelStore.getState().open(activeThreadRef, "plan"); } } diff --git a/apps/web/src/planSidebarDismissal.ts b/apps/web/src/planSidebarDismissal.ts new file mode 100644 index 00000000000..b92cfaf899e --- /dev/null +++ b/apps/web/src/planSidebarDismissal.ts @@ -0,0 +1,21 @@ +/** + * Tracks which turn's plan sidebar the user explicitly dismissed, per thread. + * + * Kept outside React state so a dismissal survives leaving and re-entering a + * thread (ChatView resets its per-thread refs on navigation). Dismissals are + * keyed by turn, so when a new turn produces fresh plan steps the sidebar + * still auto-opens. + */ +const dismissedTurnByThreadKey = new Map(); + +export function dismissPlanSidebarForTurn(threadKey: string, turnKey: string): void { + dismissedTurnByThreadKey.set(threadKey, turnKey); +} + +export function clearPlanSidebarDismissal(threadKey: string): void { + dismissedTurnByThreadKey.delete(threadKey); +} + +export function isPlanSidebarDismissedForTurn(threadKey: string, turnKey: string): boolean { + return dismissedTurnByThreadKey.get(threadKey) === turnKey; +}