feat(site): improve agent chat header design (#22621)

## Changes

- **User dropdown → sidebar bottom**: Moved from the TopBar into the
sidebar footer with avatar + display name, whole row clickable to open
the dropdown menu
- **Diff stats inline badge**: Compact green/red pill badge next to the
chat title showing `+additions −deletions`, clickable to toggle the diff
panel
- **Reordered TopBar actions**: Ellipsis menu first, then drawer toggle
button on the far right
- **Notification bell scoped**: Removed from individual chat pages
(remains on `/agents` listing)
- **Cleanup**: Removed unused `signOut`/`buildInfo` destructuring from
AgentsPage

### Files changed
- `site/src/pages/AgentsPage/AgentDetail/TopBar.tsx`
- `site/src/pages/AgentsPage/AgentsPage.tsx`
- `site/src/pages/AgentsPage/AgentsSidebar.tsx`

<img width="1876" height="1597" alt="image"
src="https://github.com/user-attachments/assets/8ec33955-f8b4-4064-9767-19147951b3ff"
/>
This commit is contained in:
Kyle Carberry
2026-03-04 13:35:55 -05:00
committed by GitHub
parent e35717bc19
commit e4bdfbebd3
5 changed files with 79 additions and 71 deletions
@@ -1,5 +1,3 @@
import { MockUserOwner } from "testHelpers/entities";
import { withAuthProvider, withDashboardProvider } from "testHelpers/storybook";
import type { Meta, StoryObj } from "@storybook/react-vite";
import type { ChatDiffStatusResponse } from "api/api";
import { expect, userEvent, waitFor, within } from "storybook/test";
@@ -40,10 +38,8 @@ const defaultProps = {
const meta: Meta<typeof AgentDetailTopBar> = {
title: "pages/AgentsPage/AgentDetail/TopBar",
component: AgentDetailTopBar,
decorators: [withAuthProvider, withDashboardProvider],
parameters: {
layout: "fullscreen",
user: MockUserOwner,
},
args: defaultProps,
};
@@ -8,7 +8,6 @@ import {
DropdownMenuSeparator,
DropdownMenuTrigger,
} from "components/DropdownMenu/DropdownMenu";
import { useAuthenticated } from "hooks";
import {
ArchiveIcon,
ArchiveRestoreIcon,
@@ -24,46 +23,30 @@ import {
TerminalIcon,
Trash2Icon,
} from "lucide-react";
import { UserDropdown } from "modules/dashboard/Navbar/UserDropdown/UserDropdown";
import { useDashboard } from "modules/dashboard/useDashboard";
import type { FC } from "react";
import { useNavigate } from "react-router";
import { toast } from "sonner";
import { ChimeButton } from "../ChimeButton";
import { WebPushButton } from "../WebPushButton";
interface DiffStatsBadgeProps {
const DiffStatsInline: FC<{
status: ChatDiffStatusResponse;
isOpen: boolean;
onToggle: () => void;
}
const DiffStatsBadge: FC<DiffStatsBadgeProps> = ({
status,
isOpen,
onToggle,
}) => {
onClick: () => void;
}> = ({ status, onClick }) => {
const additions = status.additions ?? 0;
const deletions = status.deletions ?? 0;
return (
<Button
variant="subtle"
onClick={onToggle}
className="gap-3 px-2 py-1 text-content-secondary hover:text-content-primary"
<button
type="button"
onClick={onClick}
className="inline-flex shrink-0 cursor-pointer items-center overflow-hidden rounded-md border-0 bg-transparent p-0 font-mono text-[13px] leading-none tabular-nums transition-opacity hover:opacity-80"
>
<span className="font-mono text-sm font-semibold text-content-success">
<span className="bg-content-success/10 px-1.5 py-1 text-content-success">
+{additions}
</span>
<span className="font-mono text-sm font-semibold text-content-destructive">
<span className="bg-content-destructive/10 px-1.5 py-1 text-content-destructive">
{deletions}
</span>
{isOpen ? (
<PanelRightCloseIcon className="h-4 w-4" />
) : (
<PanelRightOpenIcon className="h-4 w-4" />
)}
</Button>
</button>
);
};
@@ -113,8 +96,6 @@ export const AgentDetailTopBar: FC<AgentDetailTopBarProps> = ({
onToggleSidebarCollapsed,
}) => {
const navigate = useNavigate();
const { user, signOut } = useAuthenticated();
const { appearance, buildInfo } = useDashboard();
return (
<div className="flex shrink-0 items-center gap-2 px-4 py-0.5">
@@ -160,6 +141,14 @@ export const AgentDetailTopBar: FC<AgentDetailTopBarProps> = ({
<span className="truncate text-sm text-content-primary">
{chatTitle}
</span>
{diff.hasDiffStatus && diff.diffStatus && (
<span className="ml-3">
<DiffStatsInline
status={diff.diffStatus}
onClick={diff.onToggleFilesChanged}
/>
</span>
)}
{isArchived && (
<span className="shrink-0 rounded bg-surface-tertiary px-1.5 py-0.5 text-xs text-content-secondary">
Archived
@@ -170,13 +159,6 @@ export const AgentDetailTopBar: FC<AgentDetailTopBarProps> = ({
</div>
{/* Actions area */}
<div className="flex items-center gap-2">
{diff.hasDiffStatus && diff.diffStatus && (
<DiffStatsBadge
status={diff.diffStatus}
isOpen={diff.showDiffPanel}
onToggle={diff.onToggleFilesChanged}
/>
)}
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button
@@ -266,20 +248,21 @@ export const AgentDetailTopBar: FC<AgentDetailTopBarProps> = ({
)}
</DropdownMenuContent>
</DropdownMenu>
<ChimeButton />
<WebPushButton />{" "}
</div>
<div className="flex items-center [&_span]:!rounded-full [&_span]:!size-8 [&_span]:!text-xs">
<UserDropdown
user={user}
buildInfo={buildInfo}
supportLinks={
appearance.support_links?.filter(
(link) => link.location !== "navbar",
) ?? []
}
onSignOut={signOut}
/>
{diff.hasDiffStatus && diff.diffStatus && (
<Button
variant="subtle"
size="icon"
onClick={diff.onToggleFilesChanged}
className="h-7 w-7 text-content-secondary hover:text-content-primary"
aria-label="Toggle files changed"
>
{diff.showDiffPanel ? (
<PanelRightCloseIcon className="h-4 w-4" />
) : (
<PanelRightOpenIcon className="h-4 w-4" />
)}
</Button>
)}{" "}
</div>
</div>
);
+5 -16
View File
@@ -28,7 +28,6 @@ import {
} from "components/Select/Select";
import { useAuthenticated } from "hooks";
import { MonitorIcon, PanelLeftIcon } from "lucide-react";
import { UserDropdown } from "modules/dashboard/Navbar/UserDropdown/UserDropdown";
import { useDashboard } from "modules/dashboard/useDashboard";
import {
type FC,
@@ -47,6 +46,7 @@ import { pageTitle } from "utils/page";
import { AgentChatInput } from "./AgentChatInput";
import { maybePlayChime } from "./AgentDetail/useAgentChime";
import { AgentsSidebar } from "./AgentsSidebar";
import { ChimeButton } from "./ChimeButton";
import { ConfigureAgentsDialog } from "./ConfigureAgentsDialog";
import {
getModelCatalogStatusMessage,
@@ -104,8 +104,8 @@ const AgentsPage: FC = () => {
const queryClient = useQueryClient();
const navigate = useNavigate();
const { agentId } = useParams();
const { permissions, user, signOut } = useAuthenticated();
const { appearance, buildInfo } = useDashboard();
const { permissions, user } = useAuthenticated();
const { appearance } = useDashboard();
const isAgentsAdmin =
permissions.editDeploymentConfig ||
user.roles.some((role) => role.name === "owner" || role.name === "admin");
@@ -551,7 +551,8 @@ const AgentsPage: FC = () => {
)}
<div className="flex min-w-0 flex-1 items-center" />
<div className="flex items-center gap-2">
<WebPushButton />
<ChimeButton />
<WebPushButton />{" "}
{isAgentsAdmin && (
<Button
variant="subtle"
@@ -563,18 +564,6 @@ const AgentsPage: FC = () => {
</Button>
)}
</div>
<div className="flex items-center [&_span]:!rounded-full [&_span]:!size-8 [&_span]:!text-xs">
<UserDropdown
user={user}
buildInfo={buildInfo}
supportLinks={
appearance.support_links?.filter(
(link) => link.location !== "navbar",
) ?? []
}
onSignOut={signOut}
/>
</div>
</div>
<AgentsEmptyState
onCreateChat={handleCreateChat}
@@ -1,3 +1,5 @@
import { MockUserOwner } from "testHelpers/entities";
import { withAuthProvider, withDashboardProvider } from "testHelpers/storybook";
import type { Meta, StoryObj } from "@storybook/react-vite";
import type * as TypesGen from "api/typesGenerated";
import type { Chat } from "api/typesGenerated";
@@ -56,6 +58,7 @@ const agentsRouting = [
const meta: Meta<typeof AgentsSidebar> = {
title: "pages/AgentsPage/AgentsSidebar",
component: AgentsSidebar,
decorators: [withAuthProvider, withDashboardProvider],
args: {
chatErrorReasons: {},
modelOptions: defaultModelOptions,
@@ -68,6 +71,7 @@ const meta: Meta<typeof AgentsSidebar> = {
},
parameters: {
layout: "fullscreen",
user: MockUserOwner,
reactRouter: reactRouterParameters({
location: { path: "/agents" },
routing: agentsRouting,
+37 -1
View File
@@ -5,6 +5,7 @@ import type {
ChatStatus,
} from "api/typesGenerated";
import { ErrorAlert } from "components/Alert/ErrorAlert";
import { Avatar } from "components/Avatar/Avatar";
import type { ModelSelectorOption } from "components/ai-elements";
import { Button } from "components/Button/Button";
import {
@@ -22,6 +23,7 @@ import { ExternalImage } from "components/ExternalImage/ExternalImage";
import { CoderIcon } from "components/Icons/CoderIcon";
import { ScrollArea } from "components/ScrollArea/ScrollArea";
import { Skeleton } from "components/Skeleton/Skeleton";
import { useAuthenticated } from "hooks";
import {
AlertTriangleIcon,
ArchiveIcon,
@@ -36,6 +38,8 @@ import {
SquarePenIcon,
Trash2Icon,
} from "lucide-react";
import { UserDropdownContent } from "modules/dashboard/Navbar/UserDropdown/UserDropdownContent";
import { useDashboard } from "modules/dashboard/useDashboard";
import {
createContext,
type FC,
@@ -554,6 +558,8 @@ export const AgentsSidebar: FC<AgentsSidebarProps> = (props) => {
chatId?: string;
}>();
const activeChatId = agentId ?? chatId;
const { user, signOut } = useAuthenticated();
const { appearance, buildInfo } = useDashboard();
const normalizedSearch = "";
const [expandedById, setExpandedById] = useState<Record<string, boolean>>({});
const [isArchivedExpanded, setIsArchivedExpanded] = useState(false);
@@ -690,7 +696,6 @@ export const AgentsSidebar: FC<AgentsSidebarProps> = (props) => {
New Agent
</Button>
</div>
<ScrollArea
className="flex-1 [&_[data-radix-scroll-area-viewport]>div]:!block"
scrollBarClassName="w-1.5"
@@ -803,6 +808,37 @@ export const AgentsSidebar: FC<AgentsSidebarProps> = (props) => {
)}
</div>
</ScrollArea>
<div className="hidden border-0 border-t border-solid md:block">
<DropdownMenu>
<DropdownMenuTrigger asChild>
<button
type="button"
className="flex w-full items-center gap-2 bg-transparent border-0 cursor-pointer px-3 py-2 text-left hover:bg-surface-tertiary/50 transition-colors"
>
<Avatar
fallback={user.username}
src={user.avatar_url}
size="sm"
/>
<span className="truncate text-sm text-content-secondary">
{user.name || user.username}
</span>
</button>
</DropdownMenuTrigger>
<DropdownMenuContent align="start" className="min-w-auto w-[260px]">
<UserDropdownContent
user={user}
buildInfo={buildInfo}
supportLinks={
appearance.support_links?.filter(
(link) => link.location !== "navbar",
) ?? []
}
onSignOut={signOut}
/>
</DropdownMenuContent>
</DropdownMenu>
</div>{" "}
</div>
);
};