fix: polish agents UI (sidebar width, combobox, limits padding, back button) (#23204)

This commit is contained in:
Danielle Maywood
2026-03-18 11:46:56 +00:00
committed by GitHub
parent 4f566f92b5
commit aa3cee6410
7 changed files with 86 additions and 82 deletions
+62 -44
View File
@@ -7,15 +7,19 @@ import { ChevronDownIcon } from "components/AnimatedIcons/ChevronDown";
import type { ModelSelectorOption } from "components/ai-elements";
import { Button } from "components/Button/Button";
import {
Combobox,
ComboboxContent,
ComboboxEmpty,
ComboboxInput,
ComboboxItem,
ComboboxList,
ComboboxTrigger,
} from "components/Combobox/Combobox";
import { MonitorIcon } from "lucide-react";
Command,
CommandEmpty,
CommandGroup,
CommandInput,
CommandItem,
CommandList,
} from "components/Command/Command";
import {
Popover,
PopoverContent,
PopoverTrigger,
} from "components/Popover/Popover";
import { Check, MonitorIcon } from "lucide-react";
import { useDashboard } from "modules/dashboard/useDashboard";
import {
type FC,
@@ -191,6 +195,7 @@ export const AgentCreateForm: FC<AgentCreateFormProps> = ({
modelOptions.some((modelOption) => modelOption.id === userSelectedModel)
? userSelectedModel
: preferredModelID;
const [workspacePopoverOpen, setWorkspacePopoverOpen] = useState(false);
const workspacesQuery = useQuery(workspaces({ q: "owner:me", limit: 0 }));
const [selectedWorkspaceId, setSelectedWorkspaceId] = useState<string | null>(
() => {
@@ -377,19 +382,17 @@ export const AgentCreateForm: FC<AgentCreateFormProps> = ({
uploadStates={uploadStates}
previewUrls={previewUrls}
leftActions={
<Combobox
value={selectedWorkspaceId ?? autoCreateWorkspaceValue}
onValueChange={(value) =>
handleWorkspaceChange(value ?? autoCreateWorkspaceValue)
}
<Popover
open={workspacePopoverOpen}
onOpenChange={setWorkspacePopoverOpen}
>
{/* pointer-events-auto overrides the pointer-events:none
that Radix Select's DismissableLayer sets on
document.body when the Model Selector is open.
Without it the first click only dismisses the
Select and a second click is needed to open
the Combobox. */}
<ComboboxTrigger asChild>
that Radix Select's DismissableLayer sets on
document.body when the Model Selector is open.
Without it the first click only dismisses the
Select and a second click is needed to open
the popover. */}
<PopoverTrigger asChild>
<button
type="button"
disabled={isCreating || workspacesQuery.isLoading}
@@ -399,30 +402,45 @@ export const AgentCreateForm: FC<AgentCreateFormProps> = ({
<span>{selectedWorkspaceLabel ?? "Workspace"}</span>
<ChevronDownIcon className="size-icon-sm text-content-secondary transition-colors group-hover:text-content-primary" />
</button>
</ComboboxTrigger>
<ComboboxContent
side="top"
align="start"
className="w-72 bg-surface-primary border-border [&_[cmdk-root]]:bg-surface-primary [&_[cmdk-item]]:text-xs [&_[cmdk-item]>svg:last-child]:ml-auto"
>
<ComboboxInput placeholder="Search workspaces..." />
<ComboboxList>
<ComboboxItem value={autoCreateWorkspaceValue}>
Auto-create Workspace
</ComboboxItem>
{workspaceOptions.map((workspace) => (
<ComboboxItem
key={workspace.id}
value={workspace.id}
keywords={[workspace.owner_name, workspace.name]}
>
{workspace.owner_name}/{workspace.name}
</ComboboxItem>
))}
</ComboboxList>
<ComboboxEmpty>No workspaces found</ComboboxEmpty>
</ComboboxContent>
</Combobox>
</PopoverTrigger>
<PopoverContent side="top" align="start" className="w-72 p-0">
<Command loop>
<CommandInput placeholder="Search workspaces..." />
<CommandList>
<CommandEmpty>No workspaces found</CommandEmpty>
<CommandGroup>
<CommandItem
value="Auto-create Workspace"
onSelect={() => {
handleWorkspaceChange(autoCreateWorkspaceValue);
setWorkspacePopoverOpen(false);
}}
>
Auto-create Workspace
{selectedWorkspaceId == null && (
<Check className="ml-auto size-icon-sm shrink-0" />
)}
</CommandItem>
{workspaceOptions.map((workspace) => (
<CommandItem
key={workspace.id}
value={`${workspace.owner_name}/${workspace.name}`}
onSelect={() => {
handleWorkspaceChange(workspace.id);
setWorkspacePopoverOpen(false);
}}
>
{workspace.owner_name}/{workspace.name}
{selectedWorkspaceId === workspace.id && (
<Check className="ml-auto size-icon-sm shrink-0" />
)}
</CommandItem>
))}
</CommandGroup>
</CommandList>
</Command>
</PopoverContent>
</Popover>
}
/>
<p className="mt-1 text-center text-xs text-content-secondary/50">
+1 -1
View File
@@ -709,7 +709,7 @@ export const AgentsSidebar: FC<AgentsSidebarProps> = (props) => {
aria-hidden={sidebarView.panel === "settings"}
inert={sidebarView.panel === "settings" ? true : undefined}
>
<div className="hidden border-b border-border-default px-3 pb-3 pt-1.5 md:block md:px-3.5">
<div className="hidden border-b border-border-default px-2 pb-3 pt-1.5 md:block">
<div className="mb-2.5 flex items-center justify-between">
<NavLink to="/workspaces" className="inline-flex">
{logoUrl ? (
@@ -28,7 +28,6 @@ interface DefaultLimitSectionProps {
onAmountDollarsChange: (amount: string) => void;
unpricedModelCount: number;
adminBadge: ReactNode;
panelClassName: string;
}
export const DefaultLimitSection: FC<DefaultLimitSectionProps> = ({
@@ -40,20 +39,19 @@ export const DefaultLimitSection: FC<DefaultLimitSectionProps> = ({
onAmountDollarsChange,
unpricedModelCount,
adminBadge,
panelClassName,
}) => {
const periodId = useId();
const amountId = useId();
return (
<>
<section className="space-y-4">
<SectionHeader
label="Default Spend Limit"
description="Set a deployment-wide spend cap that applies to all users by default."
badge={adminBadge}
/>
<div className={panelClassName}>
<div className="space-y-4">
<div className="flex items-center justify-between gap-4">
<div>
<p className="m-0 text-sm font-medium text-content-primary">
@@ -134,6 +132,6 @@ export const DefaultLimitSection: FC<DefaultLimitSectionProps> = ({
</div>
</div>
)}
</>
</section>
);
};
@@ -29,7 +29,6 @@ interface GroupLimitsSectionProps {
member_count: number;
spend_limit_micros: number | null;
}>;
panelClassName: string;
showGroupForm: boolean;
onShowGroupFormChange: (show: boolean) => void;
selectedGroup: Group | null;
@@ -50,7 +49,6 @@ interface GroupLimitsSectionProps {
export const GroupLimitsSection: FC<GroupLimitsSectionProps> = ({
groupOverrides,
panelClassName,
showGroupForm,
onShowGroupFormChange,
selectedGroup,
@@ -78,7 +76,7 @@ export const GroupLimitsSection: FC<GroupLimitsSectionProps> = ({
description="Override the default limit for specific groups. When a user belongs to multiple groups, the lowest group limit applies."
/>
<div className={panelClassName}>
<div className="space-y-4">
{groupOverrides.length > 0 ? (
<Table>
<TableHeader>
@@ -30,8 +30,6 @@ import { GroupLimitsSection } from "./GroupLimitsSection";
import { normalizeChatUsageLimitPeriod } from "./limitsFormLogic";
import { UserOverridesSection } from "./UserOverridesSection";
const sectionPanelClassName = "space-y-4 rounded-lg border border-border p-4";
interface DefaultLimitFormValues {
enabled: boolean;
period: ChatUsageLimitPeriod;
@@ -305,8 +303,8 @@ export const LimitsTab: FC = () => {
saveDefault,
}) => (
<>
<div className="flex-1 overflow-y-auto px-6 py-5 pb-24 [scrollbar-width:thin] [scrollbar-color:hsl(var(--surface-quaternary))_transparent]">
<div className="space-y-6">
<div className="flex-1 overflow-y-auto pb-24 [scrollbar-width:thin] [scrollbar-color:hsl(var(--surface-quaternary))_transparent]">
<div className="space-y-10">
<DefaultLimitSection
adminBadge={<AdminBadge />}
enabled={enabled}
@@ -325,12 +323,9 @@ export const LimitsTab: FC = () => {
onAmountDollarsChange(nextAmountDollars);
}}
unpricedModelCount={unpricedModelCount}
panelClassName={sectionPanelClassName}
/>
<GroupLimitsSection
groupOverrides={groupOverrides}
panelClassName={sectionPanelClassName}
showGroupForm={showGroupForm}
onShowGroupFormChange={setShowGroupForm}
selectedGroup={selectedGroup}
@@ -358,10 +353,8 @@ export const LimitsTab: FC = () => {
}
groupsError={groupsQuery.isError ? groupsQuery.error : null}
/>
<UserOverridesSection
overrides={overrides}
panelClassName={sectionPanelClassName}
showUserForm={showUserForm}
onShowUserFormChange={setShowUserForm}
selectedUser={selectedUser}
@@ -387,7 +380,7 @@ export const LimitsTab: FC = () => {
</div>
</div>
<div className="sticky bottom-0 flex shrink-0 flex-col gap-2 border-t border-border bg-surface-primary px-6 py-3 sm:flex-row sm:items-center sm:justify-between">
<div className="sticky bottom-0 flex shrink-0 flex-col gap-2 border-t border-border bg-surface-primary py-3 sm:flex-row sm:items-center sm:justify-between">
<div className="min-h-4 text-xs">
{updateConfigMutation.isError && (
<p className="m-0 text-content-destructive">
@@ -26,7 +26,6 @@ interface UserOverridesSectionProps {
avatar_url: string;
spend_limit_micros: number | null;
}>;
panelClassName: string;
showUserForm: boolean;
onShowUserFormChange: (show: boolean) => void;
selectedUser: User | null;
@@ -44,7 +43,6 @@ interface UserOverridesSectionProps {
export const UserOverridesSection: FC<UserOverridesSectionProps> = ({
overrides,
panelClassName,
showUserForm,
onShowUserFormChange,
selectedUser,
@@ -68,7 +66,7 @@ export const UserOverridesSection: FC<UserOverridesSectionProps> = ({
description="Override the deployment default spend limit for specific users. User overrides take highest priority, followed by group limits, then the deployment default."
/>
<div className={panelClassName}>
<div className="space-y-4">
{overrides.length > 0 ? (
<Table>
<TableHeader>
@@ -35,7 +35,7 @@ import {
import dayjs from "dayjs";
import { useDebouncedValue } from "hooks/debounce";
import { useClickableTableRow } from "hooks/useClickableTableRow";
import { FlaskConicalIcon, ShieldIcon } from "lucide-react";
import { ChevronLeftIcon, FlaskConicalIcon, ShieldIcon } from "lucide-react";
import { type FC, type FormEvent, useCallback, useMemo, useState } from "react";
import {
keepPreviousData,
@@ -182,20 +182,9 @@ const UsageContent: FC<UsageContentProps> = ({ now }) => {
}
badge={<AdminBadge />}
action={
selectedUser ? (
<Button
variant="outline"
size="sm"
type="button"
onClick={() => setSelectedUser(null)}
>
← Back to all users
</Button>
) : (
<span className="text-xs text-content-secondary">
{dateRange.rangeLabel}
</span>
)
<span className="text-xs text-content-secondary">
{dateRange.rangeLabel}
</span>
}
/>
);
@@ -203,7 +192,17 @@ const UsageContent: FC<UsageContentProps> = ({ now }) => {
if (selectedUser) {
return (
<div className="space-y-6">
{header}
<div>
<button
type="button"
onClick={() => setSelectedUser(null)}
className="mb-4 inline-flex cursor-pointer items-center gap-0.5 bg-transparent border-0 p-0 text-sm text-content-secondary transition-colors hover:text-content-primary"
>
<ChevronLeftIcon className="h-4 w-4" />
Back
</button>
{header}
</div>
<div className="flex flex-wrap items-center gap-3 rounded-lg border border-border-default bg-surface-secondary px-4 py-3">
<AvatarData
title={selectedUser.name || selectedUser.username}