Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
35 changes: 20 additions & 15 deletions apps/web/src/components/ChatView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -1310,8 +1315,6 @@ function ChatViewContent(props: ChatViewProps) {
const [pendingUserInputQuestionIndexByRequestId, setPendingUserInputQuestionIndexByRequestId] =
useState<Record<string, number>>({});
const shouldUsePlanSidebarSheet = useMediaQuery(RIGHT_PANEL_INLINE_LAYOUT_MEDIA_QUERY);
// Tracks whether the user explicitly dismissed the sidebar for the active turn.
const planSidebarDismissedForTurnRef = useRef<string | null>(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);
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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();
}
Expand Down Expand Up @@ -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]);

Expand All @@ -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,
Expand Down Expand Up @@ -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");
}
}
Expand Down
21 changes: 21 additions & 0 deletions apps/web/src/planSidebarDismissal.ts
Original file line number Diff line number Diff line change
@@ -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<string, string>();

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;
}
Loading