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._
This commit is contained in:
Cian Johnston
2026-04-29 14:14:28 +01:00
committed by GitHub
parent 5ceca94e0c
commit 1856864472
8 changed files with 131 additions and 15 deletions
@@ -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<AgentAnalyticsPageProps> = ({ now }) => {
const { user } = useAuthContext();
const location = useLocation();
const [anchor] = useState<Dayjs>(() => dayjs());
const dateRange = createDateRange(now ?? anchor);
@@ -37,7 +39,12 @@ const AgentAnalyticsPage: FC<AgentAnalyticsPageProps> = ({ now }) => {
return (
<ScrollArea className="min-h-0 flex-1" viewportClassName="[&>div]:!block">
<AgentPageHeader mobileBack={{ to: "/agents", label: "Agents" }} />
<AgentPageHeader
mobileBack={{
to: { pathname: "/agents", search: location.search },
label: "Agents",
}}
/>
<AgentAnalyticsPageView
summary={summaryQuery.data}
isLoading={summaryQuery.isLoading}
+4 -3
View File
@@ -5,7 +5,7 @@ import {
useQuery,
useQueryClient,
} from "react-query";
import { useNavigate, useParams } from "react-router";
import { useLocation, useNavigate, useParams } from "react-router";
import { toast } from "sonner";
import { API, watchChats } from "#/api/api";
import { getErrorMessage } from "#/api/errors";
@@ -63,6 +63,7 @@ const AgentsPage: FC = () => {
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(() => {
@@ -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<typeof useWebpushNotifications>;
@@ -167,13 +173,18 @@ export const AgentPageHeader: FC<AgentPageHeaderProps> = ({
className="mobile-full-width-dropdown mobile-full-width-dropdown-top [&_[role=menuitem]]:text-sm"
>
<DropdownMenuItem asChild>
<Link to="/agents/settings" state={{ from: location.pathname }}>
<Link
to="/agents/settings"
state={{ from: location.pathname + location.search }}
>
<SettingsIcon className="size-icon-sm" />
Settings
</Link>
</DropdownMenuItem>
<DropdownMenuItem asChild>
<Link to="/agents/analytics">
<Link
to={{ pathname: "/agents/analytics", search: location.search }}
>
<BarChart3Icon className="size-icon-sm" />
Analytics
</Link>
@@ -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 && (
<Link
to={`/agents/${chatId}`}
to={{ pathname: `/agents/${chatId}`, search: location.search }}
onClick={(e) => e.stopPropagation()}
className="ml-1 inline-flex align-middle text-content-secondary opacity-50 transition-opacity hover:opacity-100"
aria-label="View agent"
@@ -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 <div data-testid="agents-search">{location.search}</div>;
};
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: <AgentsSearchProbe /> },
],
}),
},
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,
@@ -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<ChatTopBarProps> = ({
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<ChatTopBarProps> = ({
size="icon"
className="inline-flex h-7 w-7 min-w-0 shrink-0 md:hidden"
>
<Link to="/agents" aria-label="Back">
<Link
to={{ pathname: "/agents", search: location.search }}
aria-label="Back"
>
<ArrowLeftIcon />
</Link>
</Button>
@@ -121,7 +125,12 @@ export const ChatTopBar: FC<ChatTopBarProps> = ({
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"
>
<Link to={`/agents/${parentChat.id}`}>
<Link
to={{
pathname: `/agents/${parentChat.id}`,
search: location.search,
}}
>
<span className="truncate">{parentChat.title}</span>
</Link>
</Button>
@@ -20,6 +20,14 @@ const ChildSearchProbe = () => {
return <div data-testid="child-search">{location.search}</div>;
};
// 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 <div data-testid="settings-state-from">{from}</div>;
};
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: <SettingsStateProbe />,
},
...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");
});
},
};
@@ -1105,7 +1105,10 @@ export const AgentsSidebar: FC<AgentsSidebarProps> = (props) => {
isSettingsPanel && "text-content-primary",
)}
>
<Link to="/agents/settings" state={{ from: location.pathname }}>
<Link
to="/agents/settings"
state={{ from: location.pathname + location.search }}
>
<SettingsIcon />
</Link>
</Button>
@@ -1126,7 +1129,7 @@ export const AgentsSidebar: FC<AgentsSidebarProps> = (props) => {
icon={SquarePenIcon}
label="New Agent"
active={!activeChatId && sidebarView.panel === "chats"}
to="/agents"
to={`/agents${location.search}`}
onClick={onBeforeNewAgent}
disabled={isCreating}
/>