UI: mobile responsive layout, streamline agent budget display, and xs avatar size

Make agents list force list view on mobile with condensed trailing
info. Add mobile bottom bar for config save/cancel and live run
indicator on agent detail. Make MetricCard, PageTabBar, Dashboard
tasks, and ActivityRow responsive for small screens. Add xs avatar
size for inline text flow. Remove redundant budget displays from
agent overview, properties panel, costs tab, and config form.
Add attachment activity verb labels.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Forgotten
2026-02-20 11:29:13 -06:00
parent a22af8f72f
commit 39f8d38528
11 changed files with 217 additions and 258 deletions

View File

@@ -11,6 +11,8 @@ const ACTION_VERBS: Record<string, string> = {
"issue.checked_out": "checked out",
"issue.released": "released",
"issue.comment_added": "commented on",
"issue.attachment_added": "attached file to",
"issue.attachment_removed": "removed attachment from",
"issue.commented": "commented on",
"issue.deleted": "deleted",
"agent.created": "created",
@@ -105,23 +107,24 @@ export function ActivityRow({ event, agentMap, entityNameMap, className }: Activ
return (
<div
className={cn(
"px-4 py-2 flex flex-wrap items-center justify-between gap-x-2 gap-y-0.5 text-sm",
"px-4 py-2 text-sm",
link && "cursor-pointer hover:bg-accent/50 transition-colors",
className,
)}
onClick={link ? () => navigate(link) : undefined}
>
<div className="flex items-center gap-1.5 min-w-0">
<Identity
name={actor?.name ?? (event.actorType === "system" ? "System" : event.actorId || "You")}
size="sm"
/>
<span className="text-muted-foreground shrink-0">{verb}</span>
{name && <span className="truncate">{name}</span>}
<div className="flex gap-3">
<p className="flex-1 min-w-0">
<Identity
name={actor?.name ?? (event.actorType === "system" ? "System" : event.actorId || "You")}
size="xs"
className="align-baseline"
/>
<span className="text-muted-foreground ml-1">{verb} </span>
{name && <span className="font-medium">{name}</span>}
</p>
<span className="text-xs text-muted-foreground shrink-0 pt-0.5">{timeAgo(event.createdAt)}</span>
</div>
<span className="text-xs text-muted-foreground shrink-0">
{timeAgo(event.createdAt)}
</span>
</div>
);
}

View File

@@ -716,26 +716,6 @@ export function AgentConfigForm(props: AgentConfigFormProps) {
</div>
)}
{/* ---- Runtime (edit only) ---- */}
{!isCreate && (
<div className="border-b border-border">
<div className="px-4 py-2 text-xs font-medium text-muted-foreground">Runtime</div>
<div className="px-4 pb-3 space-y-3">
<Field label="Monthly budget (cents)" hint={help.budgetMonthlyCents}>
<DraftNumberInput
value={eff(
"runtime",
"budgetMonthlyCents",
props.agent.budgetMonthlyCents,
)}
onCommit={(v) => mark("runtime", "budgetMonthlyCents", v)}
immediate
className={inputClass}
/>
</Field>
</div>
</div>
)}
</div>
);
}

View File

@@ -6,7 +6,7 @@ import { useCompany } from "../context/CompanyContext";
import { queryKeys } from "../lib/queryKeys";
import { StatusBadge } from "./StatusBadge";
import { Identity } from "./Identity";
import { formatCents, formatDate } from "../lib/utils";
import { formatDate } from "../lib/utils";
import { Separator } from "@/components/ui/separator";
interface AgentPropertiesProps {
@@ -62,24 +62,6 @@ export function AgentProperties({ agent, runtimeState }: AgentPropertiesProps) {
<Separator />
<div className="space-y-1">
<PropertyRow label="Budget">
<span className="text-sm">
{formatCents(agent.spentMonthlyCents)} / {formatCents(agent.budgetMonthlyCents)}
</span>
</PropertyRow>
<PropertyRow label="Utilization">
<span className="text-sm">
{agent.budgetMonthlyCents > 0
? Math.round((agent.spentMonthlyCents / agent.budgetMonthlyCents) * 100)
: 0}
%
</span>
</PropertyRow>
</div>
<Separator />
<div className="space-y-1">
{(runtimeState?.sessionDisplayId ?? runtimeState?.sessionId) && (
<PropertyRow label="Session">

View File

@@ -1,7 +1,7 @@
import { cn } from "@/lib/utils";
import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar";
type IdentitySize = "sm" | "default" | "lg";
type IdentitySize = "xs" | "sm" | "default" | "lg";
export interface IdentityProps {
name: string;
@@ -18,6 +18,7 @@ function deriveInitials(name: string): string {
}
const textSize: Record<IdentitySize, string> = {
xs: "text-sm",
sm: "text-xs",
default: "text-sm",
lg: "text-sm",
@@ -27,8 +28,8 @@ export function Identity({ name, avatarUrl, initials, size = "default", classNam
const displayInitials = initials ?? deriveInitials(name);
return (
<span className={cn("inline-flex items-center gap-1.5", size === "lg" && "gap-2", className)}>
<Avatar size={size}>
<span className={cn("inline-flex gap-1.5", size === "xs" ? "items-baseline gap-1" : "items-center", size === "lg" && "gap-2", className)}>
<Avatar size={size} className={size === "xs" ? "relative top-[2px]" : undefined}>
{avatarUrl && <AvatarImage src={avatarUrl} alt={name} />}
<AvatarFallback>{displayInitials}</AvatarFallback>
</Avatar>

View File

@@ -13,27 +13,27 @@ interface MetricCardProps {
export function MetricCard({ icon: Icon, value, label, description, onClick }: MetricCardProps) {
return (
<Card>
<CardContent className="p-4">
<div className="flex gap-3">
<CardContent className="p-3 sm:p-4">
<div className="flex gap-2 sm:gap-3">
<div className="flex-1 min-w-0">
<p
className={`text-2xl font-bold${onClick ? " cursor-pointer" : ""}`}
className={`text-lg sm:text-2xl font-bold${onClick ? " cursor-pointer" : ""}`}
onClick={onClick}
>
{value}
</p>
<p
className={`text-sm text-muted-foreground${onClick ? " cursor-pointer" : ""}`}
className={`text-xs sm:text-sm text-muted-foreground${onClick ? " cursor-pointer" : ""}`}
onClick={onClick}
>
{label}
</p>
{description && (
<div className="text-xs text-muted-foreground mt-1">{description}</div>
<div className="text-[11px] sm:text-xs text-muted-foreground mt-1 hidden sm:block">{description}</div>
)}
</div>
<div className="bg-muted p-2 rounded-md h-fit shrink-0">
<Icon className="h-4 w-4 text-muted-foreground" />
<div className="bg-muted p-1.5 sm:p-2 rounded-md h-fit shrink-0">
<Icon className="h-3.5 w-3.5 sm:h-4 sm:w-4 text-muted-foreground" />
</div>
</div>
</CardContent>

View File

@@ -21,7 +21,7 @@ export function PageTabBar({ items, value, onValueChange }: PageTabBarProps) {
<select
value={value}
onChange={(e) => onValueChange(e.target.value)}
className="h-8 rounded-md border border-border bg-background px-2 py-1 text-sm focus:outline-none focus:ring-1 focus:ring-ring"
className="h-9 rounded-md border border-border bg-background px-2 py-1 text-base focus:outline-none focus:ring-1 focus:ring-ring"
>
{items.map((item) => (
<option key={item.value} value={item.value}>

View File

@@ -8,14 +8,14 @@ function Avatar({
size = "default",
...props
}: React.ComponentProps<typeof AvatarPrimitive.Root> & {
size?: "default" | "sm" | "lg"
size?: "default" | "xs" | "sm" | "lg"
}) {
return (
<AvatarPrimitive.Root
data-slot="avatar"
data-size={size}
className={cn(
"group/avatar relative flex size-8 shrink-0 overflow-hidden rounded-full select-none data-[size=lg]:size-10 data-[size=sm]:size-6",
"group/avatar relative flex size-8 shrink-0 overflow-hidden rounded-full select-none data-[size=lg]:size-10 data-[size=sm]:size-6 data-[size=xs]:size-5",
className
)}
{...props}
@@ -44,7 +44,7 @@ function AvatarFallback({
<AvatarPrimitive.Fallback
data-slot="avatar-fallback"
className={cn(
"bg-muted text-muted-foreground flex size-full items-center justify-center rounded-full text-sm group-data-[size=sm]/avatar:text-xs",
"bg-muted text-muted-foreground flex size-full items-center justify-center rounded-full text-sm group-data-[size=sm]/avatar:text-xs group-data-[size=xs]/avatar:text-[10px]",
className
)}
{...props}