mirror of
https://github.com/coder/coder.git
synced 2026-09-21 20:51:01 +08:00
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:
@@ -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>
|
||||
);
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user