fix(agents): persist right panel open/closed state to localStorage (#22906)

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
This commit is contained in:
Kyle Carberry
2026-03-10 08:43:55 -07:00
committed by GitHub
parent f22450f29b
commit 30a63009aa
3 changed files with 43 additions and 30 deletions
@@ -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(
{
@@ -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: {
+25 -30
View File
@@ -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<AgentDetailViewProps> = ({
agentId,
chatTitle,
@@ -160,38 +163,30 @@ export const AgentDetailView: FC<AgentDetailViewProps> = ({
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<boolean | null>(
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<AgentDetailViewProps> = ({
onOpenParentChat={(chatId) => onNavigateToChat(chatId)}
panel={{
showSidebarPanel,
onToggleSidebar: () => setShowSidebarPanel((prev) => !prev),
onToggleSidebar: () => handleSetShowSidebarPanel((prev) => !prev),
}}
workspace={{
canOpenEditors,
@@ -326,7 +321,7 @@ export const AgentDetailView: FC<AgentDetailViewProps> = ({
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<AgentDetailViewProps> = ({
),
},
]}
onClose={() => setShowSidebarPanel(false)}
onClose={() => handleSetShowSidebarPanel(false)}
isExpanded={visualExpanded}
onToggleExpanded={() => setIsRightPanelExpanded((prev) => !prev)}
isSidebarCollapsed={isSidebarCollapsed}