From 1856864472cbc775d1c42dae4512241ed1ea8169 Mon Sep 17 00:00:00 2001 From: Cian Johnston Date: Wed, 29 Apr 2026 14:14:28 +0100 Subject: [PATCH] fix(site/src/pages/AgentsPage): preserve ?archived in sibling navigation (#24777) Follow-up to #24742. Navigation paths were dropping `?archived` search params, silently resetting the filter now that it's URL-derived. Fixed all sibling navigation that used bare `/agents` paths or stored only `location.pathname` without `location.search`: - **ChatTopBar** mobile back button (`md:hidden`) - **ChatTopBar** parent chat breadcrumb - **AgentsSidebar** settings gear link (stored `state.from` without search) - **AgentPageHeader** mobile menu settings link (same `state.from` bug) - **AgentAnalyticsPage** mobile back button (widened `mobileBack.to` type to accept `To`) - **SubagentTool** "View agent" external link - **AgentsPage** `navigateAfterArchive` and `handleNewAgent` navigate calls Two Storybook play stories cover the ChatTopBar back button and the sidebar settings round-trip. _Generated by Coder Agents._ --- .../pages/AgentsPage/AgentAnalyticsPage.tsx | 9 +++- site/src/pages/AgentsPage/AgentsPage.tsx | 7 +-- .../AgentsPage/components/AgentPageHeader.tsx | 19 ++++++-- .../ChatElements/tools/SubagentTool.tsx | 5 +- .../components/ChatTopBar.stories.tsx | 36 ++++++++++++++ .../AgentsPage/components/ChatTopBar.tsx | 15 ++++-- .../Sidebar/AgentsSidebar.stories.tsx | 48 +++++++++++++++++++ .../components/Sidebar/AgentsSidebar.tsx | 7 ++- 8 files changed, 131 insertions(+), 15 deletions(-) diff --git a/site/src/pages/AgentsPage/AgentAnalyticsPage.tsx b/site/src/pages/AgentsPage/AgentAnalyticsPage.tsx index c1875ab92d..632fe01729 100644 --- a/site/src/pages/AgentsPage/AgentAnalyticsPage.tsx +++ b/site/src/pages/AgentsPage/AgentAnalyticsPage.tsx @@ -1,6 +1,7 @@ import dayjs, { type Dayjs } from "dayjs"; import { type FC, useState } from "react"; import { useQuery } from "react-query"; +import { useLocation } from "react-router"; import { chatCostSummary } from "#/api/queries/chats"; import { ScrollArea } from "#/components/ScrollArea/ScrollArea"; import { useAuthContext } from "#/contexts/auth/AuthProvider"; @@ -24,6 +25,7 @@ interface AgentAnalyticsPageProps { const AgentAnalyticsPage: FC = ({ now }) => { const { user } = useAuthContext(); + const location = useLocation(); const [anchor] = useState(() => dayjs()); const dateRange = createDateRange(now ?? anchor); @@ -37,7 +39,12 @@ const AgentAnalyticsPage: FC = ({ now }) => { return ( - + { useAgentsPWA(); const queryClient = useQueryClient(); const navigate = useNavigate(); + const location = useLocation(); const { agentId } = useParams(); const { permissions } = useAuthenticated(); const { appearance } = useDashboard(); @@ -321,7 +322,7 @@ const AgentsPage: FC = () => { : undefined, ) ) { - navigate("/agents"); + navigate({ pathname: "/agents", search: location.search }); } }; @@ -454,7 +455,7 @@ const AgentsPage: FC = () => { if (!agentId) { localStorage.removeItem(emptyInputStorageKey); } - navigate("/agents"); + navigate({ pathname: "/agents", search: location.search }); }; useEffect(() => { diff --git a/site/src/pages/AgentsPage/components/AgentPageHeader.tsx b/site/src/pages/AgentsPage/components/AgentPageHeader.tsx index 5083b8e9f1..52af681d52 100644 --- a/site/src/pages/AgentsPage/components/AgentPageHeader.tsx +++ b/site/src/pages/AgentsPage/components/AgentPageHeader.tsx @@ -11,7 +11,13 @@ import { } from "lucide-react"; import type { FC, ReactNode } from "react"; import { useEffect, useState } from "react"; -import { Link, NavLink, useLocation, useOutletContext } from "react-router"; +import { + Link, + NavLink, + type To, + useLocation, + useOutletContext, +} from "react-router"; import { toast } from "sonner"; import { getErrorMessage } from "#/api/errors"; import { Button } from "#/components/Button/Button"; @@ -33,7 +39,7 @@ interface AgentPageHeaderProps { children?: ReactNode; /** When set, shows a back link on mobile instead of the logo * and hides the settings/analytics nav buttons. */ - mobileBack?: { to: string; label: string }; + mobileBack?: { to: To; label: string }; chimeEnabled?: boolean; onToggleChime?: () => void; webPush?: ReturnType; @@ -167,13 +173,18 @@ export const AgentPageHeader: FC = ({ className="mobile-full-width-dropdown mobile-full-width-dropdown-top [&_[role=menuitem]]:text-sm" > - + Settings - + Analytics diff --git a/site/src/pages/AgentsPage/components/ChatElements/tools/SubagentTool.tsx b/site/src/pages/AgentsPage/components/ChatElements/tools/SubagentTool.tsx index 883d14dd19..a39f926f74 100644 --- a/site/src/pages/AgentsPage/components/ChatElements/tools/SubagentTool.tsx +++ b/site/src/pages/AgentsPage/components/ChatElements/tools/SubagentTool.tsx @@ -9,7 +9,7 @@ import { } from "lucide-react"; import type React from "react"; import { useState } from "react"; -import { Link } from "react-router"; +import { Link, useLocation } from "react-router"; import { ScrollArea } from "#/components/ScrollArea/ScrollArea"; import { cn } from "#/utils/cn"; import { Response } from "../Response"; @@ -179,6 +179,7 @@ export const SubagentTool: React.FC<{ recordingFileId, thumbnailFileId, }) => { + const location = useLocation(); const [expanded, setExpanded] = useState(false); const { desktopChatId, onOpenDesktop } = useDesktopPanel(); const hasPrompt = Boolean(prompt?.trim()); @@ -218,7 +219,7 @@ export const SubagentTool: React.FC<{ )} {chatId && ( e.stopPropagation()} className="ml-1 inline-flex align-middle text-content-secondary opacity-50 transition-opacity hover:opacity-100" aria-label="View agent" diff --git a/site/src/pages/AgentsPage/components/ChatTopBar.stories.tsx b/site/src/pages/AgentsPage/components/ChatTopBar.stories.tsx index fa827a5753..37d88aa1f2 100644 --- a/site/src/pages/AgentsPage/components/ChatTopBar.stories.tsx +++ b/site/src/pages/AgentsPage/components/ChatTopBar.stories.tsx @@ -1,7 +1,16 @@ import type { Meta, StoryObj } from "@storybook/react-vite"; +import { useLocation } from "react-router"; import { expect, fn, userEvent, waitFor, within } from "storybook/test"; +import { reactRouterParameters } from "storybook-addon-remix-react-router"; import { ChatTopBar } from "./ChatTopBar"; +// Probe element rendered at /agents to verify search params are preserved +// when the mobile back button navigates away from a chat. +const AgentsSearchProbe = () => { + const location = useLocation(); + return
{location.search}
; +}; + const defaultProps = { chatTitle: "Build authentication feature", panel: { @@ -243,6 +252,33 @@ export const GenerateTitle: Story = { }, }; +export const PreservesArchivedFilterOnMobileBack: Story = { + decorators: mobileDecorator, + parameters: { + chromatic: { viewports: [390] }, + reactRouter: reactRouterParameters({ + location: { + path: "/agents/chat-123", + searchParams: { archived: "archived" }, + }, + routing: [ + { path: "/agents/:agentId", useStoryElement: true }, + { path: "/agents", element: }, + ], + }), + }, + play: async ({ canvasElement }) => { + const canvas = within(canvasElement); + const backLink = await canvas.findByLabelText("Back"); + await userEvent.click(backLink); + await waitFor(() => { + expect(canvas.getByTestId("agents-search")).toHaveTextContent( + "?archived=archived", + ); + }); + }, +}; + export const ArchivedWithUnarchive: Story = { args: { isArchived: true, diff --git a/site/src/pages/AgentsPage/components/ChatTopBar.tsx b/site/src/pages/AgentsPage/components/ChatTopBar.tsx index e30a69c5f2..75c8106509 100644 --- a/site/src/pages/AgentsPage/components/ChatTopBar.tsx +++ b/site/src/pages/AgentsPage/components/ChatTopBar.tsx @@ -11,7 +11,7 @@ import { WandSparklesIcon, } from "lucide-react"; import type { FC } from "react"; -import { Link } from "react-router"; +import { Link, useLocation } from "react-router"; import type * as TypesGen from "#/api/typesGenerated"; import type { ChatDiffStatus } from "#/api/typesGenerated"; import { Button } from "#/components/Button/Button"; @@ -67,6 +67,7 @@ export const ChatTopBar: FC = ({ diffStatusData, }) => { const { isEmbedded } = useEmbedContext(); + const location = useLocation(); const prUrl = diffStatusData?.url; const prState = diffStatusData?.pull_request_state; @@ -87,7 +88,10 @@ export const ChatTopBar: FC = ({ size="icon" className="inline-flex h-7 w-7 min-w-0 shrink-0 md:hidden" > - + @@ -121,7 +125,12 @@ export const ChatTopBar: FC = ({ variant="subtle" className="h-auto max-w-[16rem] rounded-sm px-1 py-0.5 text-sm text-content-secondary shadow-none hover:bg-transparent hover:text-content-primary" > - + {parentChat.title} diff --git a/site/src/pages/AgentsPage/components/Sidebar/AgentsSidebar.stories.tsx b/site/src/pages/AgentsPage/components/Sidebar/AgentsSidebar.stories.tsx index ac109c470e..524d4a832b 100644 --- a/site/src/pages/AgentsPage/components/Sidebar/AgentsSidebar.stories.tsx +++ b/site/src/pages/AgentsPage/components/Sidebar/AgentsSidebar.stories.tsx @@ -20,6 +20,14 @@ const ChildSearchProbe = () => { return
{location.search}
; }; +// Probe element used by the settings-link preservation story to surface the +// state.from value passed when navigating to settings. +const SettingsStateProbe = () => { + const location = useLocation(); + const from = (location.state as { from?: string })?.from ?? ""; + return
{from}
; +}; + const defaultModelOptions: ModelSelectorOption[] = [ { id: "openai:gpt-4o", @@ -1594,3 +1602,43 @@ export const SettingsAPIKeysNonAdmin: Story = { ).toBeInTheDocument(); }, }; + +export const PreservesArchivedFilterOnSettingsNavigation: Story = { + args: { + chats: [ + buildChat({ + id: "archived-settings-1", + title: "Archived settings target", + archived: true, + updated_at: recentTimestamp, + }), + ], + archivedFilter: "archived", + }, + parameters: { + reactRouter: reactRouterParameters({ + location: { + path: "/agents", + searchParams: { archived: "archived" }, + }, + routing: [ + { + path: "/agents/settings", + element: , + }, + ...agentsRouting, + ], + }), + }, + play: async ({ canvasElement }) => { + const canvas = within(canvasElement); + const settingsLink = await canvas.findByLabelText("Settings"); + await userEvent.click(settingsLink); + await waitFor(() => { + const fromValue = + canvas.getByTestId("settings-state-from").textContent ?? ""; + expect(fromValue).toContain("/agents"); + expect(fromValue).toContain("archived=archived"); + }); + }, +}; diff --git a/site/src/pages/AgentsPage/components/Sidebar/AgentsSidebar.tsx b/site/src/pages/AgentsPage/components/Sidebar/AgentsSidebar.tsx index 197efefc17..6c344864a1 100644 --- a/site/src/pages/AgentsPage/components/Sidebar/AgentsSidebar.tsx +++ b/site/src/pages/AgentsPage/components/Sidebar/AgentsSidebar.tsx @@ -1105,7 +1105,10 @@ export const AgentsSidebar: FC = (props) => { isSettingsPanel && "text-content-primary", )} > - + @@ -1126,7 +1129,7 @@ export const AgentsSidebar: FC = (props) => { icon={SquarePenIcon} label="New Agent" active={!activeChatId && sidebarView.panel === "chats"} - to="/agents" + to={`/agents${location.search}`} onClick={onBeforeNewAgent} disabled={isCreating} />