Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 28 additions & 2 deletions frontend/src/app/dashboard/components/AgentsTab.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ import { useState, useEffect, useRef } from "react";
import { Button } from "@/components/ui/button";
import { Card, CardContent } from "@/components/ui/card";
import { Badge } from "@/components/ui/badge";
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@/components/ui/tooltip";
import { Info } from "lucide-react";
import { Agent, Alert, AlertDetails, CostData, AgentStatus, AlertSeverity, SpendData } from "@/lib/types";

// --- Config ---
Expand Down Expand Up @@ -143,7 +145,7 @@ export function AgentsTab({
const aggregatedAlerts = aggregateAlerts(alerts);

return (
<>
<TooltipProvider>
{/* Agent List */}
<div>
<div className="flex items-center justify-between mb-4">
Expand Down Expand Up @@ -172,6 +174,30 @@ export function AgentsTab({
</div>
<div className="flex items-center gap-2">
<Badge variant="outline" className={`${sc.color} border text-xs`}>{sc.label}</Badge>
{agent.status === "running" && (
<Tooltip>
<TooltipTrigger
render={<span className="inline-flex items-center cursor-help" aria-label="About Running status" />}
Copy link

Copilot AI Apr 13, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

TooltipTrigger is rendered as a <span> via the render prop, which is not keyboard-focusable. This makes the tooltip inaccessible to keyboard users (and the aria-label on a non-interactive element won’t be announced reliably). Consider using the trigger’s default interactive element (if it defaults to a <button>), or render a <button type="button"> (or add tabIndex={0} + appropriate role/keyboard handlers) and ensure it has a visible focus style.

Suggested change
render={<span className="inline-flex items-center cursor-help" aria-label="About Running status" />}
render={
<button
type="button"
className="inline-flex items-center cursor-help rounded-sm focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2"
aria-label="About Running status"
/>
}

Copilot uses AI. Check for mistakes.
>
<Info className="size-3.5 text-muted-foreground hover:text-foreground transition-colors" />
</TooltipTrigger>
<TooltipContent side="top" className="max-w-xs">
<div className="space-y-1.5">
<p className="font-medium">Running Status</p>
<p className="text-xs text-muted-foreground">
This agent has an active session with messages exchanged in the last few minutes.
</p>
<p className="text-xs text-muted-foreground">
<span className="font-medium">How it differs:</span> &quot;Idle&quot; means no active sessions,
&quot;Completed&quot; means the session finished, and &quot;Stopped&quot; means the agent process isn&apos;t running.
Comment on lines +191 to +192
Copy link

Copilot AI Apr 13, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The tooltip copy references a “Completed” status, but AgentStatus (and the status badges in this list) don’t include a completed state. This is likely to confuse users; consider updating the text to reference the actual agent statuses shown here (e.g., Active/Idle/Paused/Stopped) or explicitly call out that “completed” refers to a session status instead of an agent status.

Suggested change
<span className="font-medium">How it differs:</span> &quot;Idle&quot; means no active sessions,
&quot;Completed&quot; means the session finished, and &quot;Stopped&quot; means the agent process isn&apos;t running.
<span className="font-medium">How it differs:</span> &quot;Paused&quot; means the agent is temporarily halted,
and &quot;Stopped&quot; means the agent process isn&apos;t running.

Copilot uses AI. Check for mistakes.
</p>
<p className="text-xs text-muted-foreground">
Status updates automatically based on heartbeat messages from the agent.
</p>
</div>
</TooltipContent>
</Tooltip>
)}
{agent.overLimit && agent.limit != null && (() => {
const spend = agent.limitType === "daily" ? (agent.todaySpend ?? 0) : (agent.mtdSpend ?? 0);
const over = spend - agent.limit;
Expand Down Expand Up @@ -400,6 +426,6 @@ export function AgentsTab({
</div>
)}
</div>
</>
</TooltipProvider>
);
}
66 changes: 66 additions & 0 deletions frontend/src/components/ui/tooltip.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
"use client"

import { Tooltip as TooltipPrimitive } from "@base-ui/react/tooltip"

import { cn } from "@/lib/utils"

function TooltipProvider({
delay = 0,
...props
}: TooltipPrimitive.Provider.Props) {
return (
<TooltipPrimitive.Provider
data-slot="tooltip-provider"
delay={delay}
{...props}
/>
)
}

function Tooltip({ ...props }: TooltipPrimitive.Root.Props) {
return <TooltipPrimitive.Root data-slot="tooltip" {...props} />
}

function TooltipTrigger({ ...props }: TooltipPrimitive.Trigger.Props) {
return <TooltipPrimitive.Trigger data-slot="tooltip-trigger" {...props} />
}

function TooltipContent({
className,
side = "top",
sideOffset = 4,
align = "center",
alignOffset = 0,
children,
...props
}: TooltipPrimitive.Popup.Props &
Pick<
TooltipPrimitive.Positioner.Props,
"align" | "alignOffset" | "side" | "sideOffset"
>) {
return (
<TooltipPrimitive.Portal>
<TooltipPrimitive.Positioner
align={align}
alignOffset={alignOffset}
side={side}
sideOffset={sideOffset}
className="isolate z-50"
>
<TooltipPrimitive.Popup
data-slot="tooltip-content"
className={cn(
"z-50 inline-flex w-fit max-w-xs origin-(--transform-origin) items-center gap-1.5 rounded-md bg-foreground px-3 py-1.5 text-xs text-background has-data-[slot=kbd]:pr-1.5 data-[side=bottom]:slide-in-from-top-2 data-[side=inline-end]:slide-in-from-left-2 data-[side=inline-start]:slide-in-from-right-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 **:data-[slot=kbd]:relative **:data-[slot=kbd]:isolate **:data-[slot=kbd]:z-50 **:data-[slot=kbd]:rounded-sm data-[state=delayed-open]:animate-in data-[state=delayed-open]:fade-in-0 data-[state=delayed-open]:zoom-in-95 data-open:animate-in data-open:fade-in-0 data-open:zoom-in-95 data-closed:animate-out data-closed:fade-out-0 data-closed:zoom-out-95",
className
)}
{...props}
>
{children}
<TooltipPrimitive.Arrow className="z-50 size-2.5 translate-y-[calc(-50%-2px)] rotate-45 rounded-[2px] bg-foreground fill-foreground data-[side=bottom]:top-1 data-[side=inline-end]:top-1/2! data-[side=inline-end]:-left-1 data-[side=inline-end]:-translate-y-1/2 data-[side=inline-start]:top-1/2! data-[side=inline-start]:-right-1 data-[side=inline-start]:-translate-y-1/2 data-[side=left]:top-1/2! data-[side=left]:-right-1 data-[side=left]:-translate-y-1/2 data-[side=right]:top-1/2! data-[side=right]:-left-1 data-[side=right]:-translate-y-1/2 data-[side=top]:-bottom-2.5" />
</TooltipPrimitive.Popup>
</TooltipPrimitive.Positioner>
</TooltipPrimitive.Portal>
)
}

export { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider }