authorgravatar for me@t3.ggTheo Browne <me@t3.gg> 2026-08-06 05:37:27-04:00
committergravatar for noreply@github.comGitHub <noreply@github.com> 2026-08-06 02:37:27-07:00
log1ffba7093a83c99ef41dd486b10df75f2b9974e1
tree7d329ef53d5133b08a7ea20803e125c8aa41aa47
parent3da315e7b5c4537cbc7280f33dadb3f5f0e3baf0
signature Signed by PGP key B5690EEEBB952194

fix(web): closed plan sidebar stays closed when returning to a thread (#5484)

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>

2 files changed, 41 insertions(+), 15 deletions(-)

apps/web/src/components/ChatView.tsx+20-15
...@@ -117,6 +117,11 @@ import { useTurnDiffSummaries } from "../hooks/useTurnDiffSummaries";...@@ -117,6 +117,11 @@ import { useTurnDiffSummaries } from "../hooks/useTurnDiffSummaries";
117import { isCommandPaletteOpen } from "../commandPaletteBus";117import { isCommandPaletteOpen } from "../commandPaletteBus";
118import { buildTemporaryWorktreeBranchName } from "@t3tools/shared/git";118import { buildTemporaryWorktreeBranchName } from "@t3tools/shared/git";
119import { useMediaQuery } from "../hooks/useMediaQuery";119import { useMediaQuery } from "../hooks/useMediaQuery";
120import {
121 clearPlanSidebarDismissal,
122 dismissPlanSidebarForTurn,
123 isPlanSidebarDismissedForTurn,
124} from "../planSidebarDismissal";
120import { RIGHT_PANEL_INLINE_LAYOUT_MEDIA_QUERY } from "../rightPanelLayout";125import { RIGHT_PANEL_INLINE_LAYOUT_MEDIA_QUERY } from "../rightPanelLayout";
121import {126import {
122 selectActiveRightPanel,127 selectActiveRightPanel,
...@@ -1310,8 +1315,6 @@ function ChatViewContent(props: ChatViewProps) {...@@ -1310,8 +1315,6 @@ function ChatViewContent(props: ChatViewProps) {
1310 const [pendingUserInputQuestionIndexByRequestId, setPendingUserInputQuestionIndexByRequestId] =1315 const [pendingUserInputQuestionIndexByRequestId, setPendingUserInputQuestionIndexByRequestId] =
1311 useState<Record<string, number>>({});1316 useState<Record<string, number>>({});
1312 const shouldUsePlanSidebarSheet = useMediaQuery(RIGHT_PANEL_INLINE_LAYOUT_MEDIA_QUERY);1317 const shouldUsePlanSidebarSheet = useMediaQuery(RIGHT_PANEL_INLINE_LAYOUT_MEDIA_QUERY);
1313 // Tracks whether the user explicitly dismissed the sidebar for the active turn.
1314 const planSidebarDismissedForTurnRef = useRef<string | null>(null);
1315 // When set, the thread-change reset effect will open the sidebar instead of closing it.1318 // When set, the thread-change reset effect will open the sidebar instead of closing it.
1316 // Used by "Implement in a new thread" to carry the sidebar-open intent across navigation.1319 // Used by "Implement in a new thread" to carry the sidebar-open intent across navigation.
1317 const planSidebarOpenOnNextThreadRef = useRef(false);1320 const planSidebarOpenOnNextThreadRef = useRef(false);
...@@ -3110,18 +3113,21 @@ function ChatViewContent(props: ChatViewProps) {...@@ -3110,18 +3113,21 @@ function ChatViewContent(props: ChatViewProps) {
3110 handleInteractionModeChange(interactionMode === "plan" ? "default" : "plan");3113 handleInteractionModeChange(interactionMode === "plan" ? "default" : "plan");
3111 }, [handleInteractionModeChange, interactionMode]);3114 }, [handleInteractionModeChange, interactionMode]);
3112 const dismissPlanSidebarForCurrentTurn = useCallback(() => {3115 const dismissPlanSidebarForCurrentTurn = useCallback(() => {
3113 planSidebarDismissedForTurnRef.current =3116 if (!activeThreadKey) return;
3114 activePlan?.turnId ?? sidebarProposedPlan?.turnId ?? "__dismissed__";3117 dismissPlanSidebarForTurn(
3115 }, [activePlan?.turnId, sidebarProposedPlan?.turnId]);3118 activeThreadKey,
3119 activePlan?.turnId ?? sidebarProposedPlan?.turnId ?? "__dismissed__",
3120 );
3121 }, [activeThreadKey, activePlan?.turnId, sidebarProposedPlan?.turnId]);
3116 const togglePlanSidebar = useCallback(() => {3122 const togglePlanSidebar = useCallback(() => {
3117 if (!activeThreadRef) return;3123 if (!activeThreadRef) return;
3118 if (planSidebarOpen) {3124 if (planSidebarOpen) {
3119 dismissPlanSidebarForCurrentTurn();3125 dismissPlanSidebarForCurrentTurn();
3120 } else {3126 } else if (activeThreadKey) {
3121 planSidebarDismissedForTurnRef.current = null;3127 clearPlanSidebarDismissal(activeThreadKey);
3122 }3128 }
3123 useRightPanelStore.getState().toggle(activeThreadRef, "plan");3129 useRightPanelStore.getState().toggle(activeThreadRef, "plan");
3124 }, [activeThreadRef, dismissPlanSidebarForCurrentTurn, planSidebarOpen]);3130 }, [activeThreadKey, activeThreadRef, dismissPlanSidebarForCurrentTurn, planSidebarOpen]);
3125 const closePlanSidebar = useCallback(() => {3131 const closePlanSidebar = useCallback(() => {
3126 if (!activeThreadRef) return;3132 if (!activeThreadRef) return;
3127 setMaximizedRightPanelThreadKey(null);3133 setMaximizedRightPanelThreadKey(null);
...@@ -3283,7 +3289,7 @@ function ChatViewContent(props: ChatViewProps) {...@@ -3283,7 +3289,7 @@ function ChatViewContent(props: ChatViewProps) {
3283 (surface: RightPanelSurface) => {3289 (surface: RightPanelSurface) => {
3284 if (!activeThreadRef) return;3290 if (!activeThreadRef) return;
3285 if (surface.kind === "plan") {3291 if (surface.kind === "plan") {
3286 planSidebarDismissedForTurnRef.current = null;3292 clearPlanSidebarDismissal(scopedThreadKey(activeThreadRef));
3287 } else if (planSidebarOpen) {3293 } else if (planSidebarOpen) {
3288 dismissPlanSidebarForCurrentTurn();3294 dismissPlanSidebarForCurrentTurn();
3289 }3295 }
...@@ -3852,10 +3858,10 @@ function ChatViewContent(props: ChatViewProps) {...@@ -3852,10 +3858,10 @@ function ChatViewContent(props: ChatViewProps) {
3852 if (planSidebarOpenOnNextThreadRef.current) {3858 if (planSidebarOpenOnNextThreadRef.current) {
3853 planSidebarOpenOnNextThreadRef.current = false;3859 planSidebarOpenOnNextThreadRef.current = false;
3854 if (activeThreadRef) {3860 if (activeThreadRef) {
3861 clearPlanSidebarDismissal(scopedThreadKey(activeThreadRef));
3855 useRightPanelStore.getState().open(activeThreadRef, "plan");3862 useRightPanelStore.getState().open(activeThreadRef, "plan");
3856 }3863 }
3857 }3864 }
3858 planSidebarDismissedForTurnRef.current = null;
3859 // activeThreadRef resets transitively with the active thread.3865 // activeThreadRef resets transitively with the active thread.
3860 }, [activeThread?.id]);3866 }, [activeThread?.id]);
38613867
...@@ -3868,10 +3874,9 @@ function ChatViewContent(props: ChatViewProps) {...@@ -3868,10 +3874,9 @@ function ChatViewContent(props: ChatViewProps) {
3868 const latestTurnId = activeLatestTurn?.turnId ?? null;3874 const latestTurnId = activeLatestTurn?.turnId ?? null;
3869 if (latestTurnId && activePlan.turnId !== latestTurnId) return;3875 if (latestTurnId && activePlan.turnId !== latestTurnId) return;
3870 const turnKey = activePlan.turnId ?? sidebarProposedPlan?.turnId ?? "__dismissed__";3876 const turnKey = activePlan.turnId ?? sidebarProposedPlan?.turnId ?? "__dismissed__";
3871 if (planSidebarDismissedForTurnRef.current === turnKey) return;3877 if (!activeThreadRef) return;
3872 if (activeThreadRef) {3878 if (isPlanSidebarDismissedForTurn(scopedThreadKey(activeThreadRef), turnKey)) return;
3873 useRightPanelStore.getState().open(activeThreadRef, "plan");3879 useRightPanelStore.getState().open(activeThreadRef, "plan");
3874 }
3875 }, [3880 }, [
3876 activePlan,3881 activePlan,
3877 activeLatestTurn?.turnId,3882 activeLatestTurn?.turnId,
...@@ -5412,8 +5417,8 @@ function ChatViewContent(props: ChatViewProps) {...@@ -5412,8 +5417,8 @@ function ChatViewContent(props: ChatViewProps) {
5412 // "default" mode here means the agent is executing the plan, which produces5417 // "default" mode here means the agent is executing the plan, which produces
5413 // step-tracking activities that the sidebar will display.5418 // step-tracking activities that the sidebar will display.
5414 if (nextInteractionMode === "default" && autoOpenPlanSidebar) {5419 if (nextInteractionMode === "default" && autoOpenPlanSidebar) {
5415 planSidebarDismissedForTurnRef.current = null;
5416 if (activeThreadRef) {5420 if (activeThreadRef) {
5421 clearPlanSidebarDismissal(scopedThreadKey(activeThreadRef));
5417 useRightPanelStore.getState().open(activeThreadRef, "plan");5422 useRightPanelStore.getState().open(activeThreadRef, "plan");
5418 }5423 }
5419 }5424 }
apps/web/src/planSidebarDismissal.ts created+21
...@@ -0,0 +1,21 @@
1/**
2 * Tracks which turn's plan sidebar the user explicitly dismissed, per thread.
3 *
4 * Kept outside React state so a dismissal survives leaving and re-entering a
5 * thread (ChatView resets its per-thread refs on navigation). Dismissals are
6 * keyed by turn, so when a new turn produces fresh plan steps the sidebar
7 * still auto-opens.
8 */
9const dismissedTurnByThreadKey = new Map<string, string>();
10
11export function dismissPlanSidebarForTurn(threadKey: string, turnKey: string): void {
12 dismissedTurnByThreadKey.set(threadKey, turnKey);
13}
14
15export function clearPlanSidebarDismissal(threadKey: string): void {
16 dismissedTurnByThreadKey.delete(threadKey);
17}
18
19export function isPlanSidebarDismissedForTurn(threadKey: string, turnKey: string): boolean {
20 return dismissedTurnByThreadKey.get(threadKey) === turnKey;
21}