From 30a63009aa7b3cc252db2e65a4dbbce67ba60898 Mon Sep 17 00:00:00 2001 From: Kyle Carberry Date: Tue, 10 Mar 2026 08:43:55 -0700 Subject: [PATCH] fix(agents): persist right panel open/closed state to localStorage (#22906) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Removes the auto-open/close behavior that would force the right-side panel open whenever diff status or git repository data appeared. Instead, the panel's visibility is now persisted via the `agents.right-panel-open` localStorage key (matching the existing `agents.right-panel-width` pattern for the panel width). This gives users a consistent UX when switching between chats — the panel stays in whatever state they last set it to. ## Changes - **Removed** two auto-open blocks in `AgentDetailView` that tracked `prevHasDiffStatus` / `prevHasGitRepos` and forced `showSidebarPanel = true` - **Added** `localStorage` persistence for the panel open/closed state under key `agents.right-panel-open` - Initial state is read from localStorage on mount (defaults to closed) - Every toggle/close writes through to localStorage via `handleSetShowSidebarPanel` - Panel width was already persisted via `agents.right-panel-width` in `RightPanel.tsx` — no changes needed there --- .../pages/AgentsPage/AgentDetail.stories.tsx | 13 +++++ .../AgentsPage/AgentDetailView.stories.tsx | 5 ++ site/src/pages/AgentsPage/AgentDetailView.tsx | 55 +++++++++---------- 3 files changed, 43 insertions(+), 30 deletions(-) diff --git a/site/src/pages/AgentsPage/AgentDetail.stories.tsx b/site/src/pages/AgentsPage/AgentDetail.stories.tsx index e297effc59..dbca860700 100644 --- a/site/src/pages/AgentsPage/AgentDetail.stories.tsx +++ b/site/src/pages/AgentsPage/AgentDetail.stories.tsx @@ -28,6 +28,7 @@ import { reactRouterParameters, } from "storybook-addon-remix-react-router"; import AgentDetail from "./AgentDetail"; +import { RIGHT_PANEL_OPEN_KEY } from "./AgentDetailView"; import type { AgentsOutletContext } from "./AgentsPage"; // --------------------------------------------------------------------------- @@ -305,6 +306,10 @@ export const Loading: Story = {}; /** Full layout with actions menu and diff panel portaled to the right slot. */ export const CompletedWithDiffPanel: Story = { + beforeEach: () => { + localStorage.setItem(RIGHT_PANEL_OPEN_KEY, "true"); + return () => localStorage.removeItem(RIGHT_PANEL_OPEN_KEY); + }, parameters: { queries: buildQueries( { @@ -517,6 +522,10 @@ export const StreamedSubagentTitle: Story = { * the PR tab. */ export const SidebarWithPRAndRepos: Story = { + beforeEach: () => { + localStorage.setItem(RIGHT_PANEL_OPEN_KEY, "true"); + return () => localStorage.removeItem(RIGHT_PANEL_OPEN_KEY); + }, parameters: { queries: buildQueries( { @@ -697,6 +706,10 @@ export const SidebarWithPRAndRepos: Story = { * the tab bar is hidden and the repo panel is rendered directly. */ export const SidebarWithSingleRepo: Story = { + beforeEach: () => { + localStorage.setItem(RIGHT_PANEL_OPEN_KEY, "true"); + return () => localStorage.removeItem(RIGHT_PANEL_OPEN_KEY); + }, parameters: { queries: buildQueries( { diff --git a/site/src/pages/AgentsPage/AgentDetailView.stories.tsx b/site/src/pages/AgentsPage/AgentDetailView.stories.tsx index 81ecc6d8ad..9ba3790236 100644 --- a/site/src/pages/AgentsPage/AgentDetailView.stories.tsx +++ b/site/src/pages/AgentsPage/AgentDetailView.stories.tsx @@ -11,6 +11,7 @@ import { AgentDetailLoadingView, AgentDetailNotFoundView, AgentDetailView, + RIGHT_PANEL_OPEN_KEY, } from "./AgentDetailView"; // --------------------------------------------------------------------------- @@ -191,6 +192,10 @@ export const SubmissionPending: Story = { /** Right sidebar panel is open with diff status data. */ export const WithSidebarPanel: Story = { + beforeEach: () => { + localStorage.setItem(RIGHT_PANEL_OPEN_KEY, "true"); + return () => localStorage.removeItem(RIGHT_PANEL_OPEN_KEY); + }, args: { prNumber: 123, diffStatusData: { diff --git a/site/src/pages/AgentsPage/AgentDetailView.tsx b/site/src/pages/AgentsPage/AgentDetailView.tsx index a35ea99df9..9af1957672 100644 --- a/site/src/pages/AgentsPage/AgentDetailView.tsx +++ b/site/src/pages/AgentsPage/AgentDetailView.tsx @@ -3,7 +3,7 @@ import type * as TypesGen from "api/typesGenerated"; import type { ModelSelectorOption } from "components/ai-elements"; import { Skeleton } from "components/Skeleton/Skeleton"; import { ArchiveIcon } from "lucide-react"; -import { type FC, type RefObject, useMemo, useState } from "react"; +import { type FC, type RefObject, useCallback, useMemo, useState } from "react"; import type { UrlTransform } from "streamdown"; import { cn } from "utils/cn"; import { pageTitle } from "utils/page"; @@ -115,6 +115,9 @@ interface AgentDetailViewProps { urlTransform?: UrlTransform; } +/** localStorage key controlling whether the right panel is visible. */ +export const RIGHT_PANEL_OPEN_KEY = "agents.right-panel-open"; + export const AgentDetailView: FC = ({ agentId, chatTitle, @@ -160,38 +163,30 @@ export const AgentDetailView: FC = ({ urlTransform, }) => { // Panel/sidebar UI state – purely visual, no data-fetching - // implications. - const [showSidebarPanel, setShowSidebarPanel] = useState(false); + // implications. The open/closed state is persisted to localStorage + // so users get a consistent layout when switching between chats. + const [showSidebarPanel, setShowSidebarPanel] = useState(() => { + if (typeof window === "undefined") return false; + return localStorage.getItem(RIGHT_PANEL_OPEN_KEY) === "true"; + }); + const handleSetShowSidebarPanel = useCallback( + (next: boolean | ((prev: boolean) => boolean)) => { + setShowSidebarPanel((prev) => { + const value = typeof next === "function" ? next(prev) : next; + if (typeof window !== "undefined") { + localStorage.setItem(RIGHT_PANEL_OPEN_KEY, String(value)); + } + return value; + }); + }, + [], + ); const [isRightPanelExpanded, setIsRightPanelExpanded] = useState(false); const [dragVisualExpanded, setDragVisualExpanded] = useState( null, ); const visualExpanded = dragVisualExpanded ?? isRightPanelExpanded; - // Derive trivial booleans the View can compute itself. - const hasDiffStatus = Boolean(diffStatusData?.url); - const hasGitRepos = gitWatcher.repositories.size > 0; - - // Auto-open the diff panel when diff status first appears. - // See: https://react.dev/learn/you-might-not-need-an-effect#adjusting-some-state-when-a-prop-changes - const [prevHasDiffStatus, setPrevHasDiffStatus] = useState(false); - if (hasDiffStatus !== prevHasDiffStatus) { - setPrevHasDiffStatus(hasDiffStatus); - if (hasDiffStatus && !window.matchMedia("(max-width: 767px)").matches) { - setShowSidebarPanel(true); - } - } - - // Auto-open sidebar when git watcher receives its first non-empty - // repositories update. - const [prevHasGitRepos, setPrevHasGitRepos] = useState(false); - if (hasGitRepos !== prevHasGitRepos) { - setPrevHasGitRepos(hasGitRepos); - if (hasGitRepos && !window.matchMedia("(max-width: 767px)").matches) { - setShowSidebarPanel(true); - } - } - // Compute local diff stats from git watcher unified diffs. const localDiffStats = useMemo(() => { let additions = 0; @@ -239,7 +234,7 @@ export const AgentDetailView: FC = ({ onOpenParentChat={(chatId) => onNavigateToChat(chatId)} panel={{ showSidebarPanel, - onToggleSidebar: () => setShowSidebarPanel((prev) => !prev), + onToggleSidebar: () => handleSetShowSidebarPanel((prev) => !prev), }} workspace={{ canOpenEditors, @@ -326,7 +321,7 @@ export const AgentDetailView: FC = ({ isOpen={shouldShowSidebar} isExpanded={isRightPanelExpanded} onToggleExpanded={() => setIsRightPanelExpanded((prev) => !prev)} - onClose={() => setShowSidebarPanel(false)} + onClose={() => handleSetShowSidebarPanel(false)} onVisualExpandedChange={setDragVisualExpanded} isSidebarCollapsed={isSidebarCollapsed} onToggleSidebarCollapsed={onToggleSidebarCollapsed} @@ -354,7 +349,7 @@ export const AgentDetailView: FC = ({ ), }, ]} - onClose={() => setShowSidebarPanel(false)} + onClose={() => handleSetShowSidebarPanel(false)} isExpanded={visualExpanded} onToggleExpanded={() => setIsRightPanelExpanded((prev) => !prev)} isSidebarCollapsed={isSidebarCollapsed}