feat(panel): tooltip sweep — overview, agents, A2A, journals, auditor, metrics

Derivation tips on every key-metric, scorecard, and quality-metrics
figure; the cryptic member-scorecard headers get full decodes; avatar
initials, truncated ids, and toggle buttons gain accessible names;
title-only hints upgrade to the HelpTip idiom throughout.
This commit is contained in:
Renn F
2026-07-15 16:40:13 +02:00
parent c37516f640
commit 3d1c20e95a
33 changed files with 723 additions and 334 deletions
@@ -1,5 +1,6 @@
import { describe, it, expect, vi, beforeEach } from "vitest";
import { render, screen } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import { AgentRole, AgentState, type Agent } from "@/types";
import type { MemberScorecard } from "@/types";
@@ -139,6 +140,15 @@ describe("ScorecardsTabContent", () => {
expect(screen.queryByText("system")).not.toBeInTheDocument();
});
it("explains an abbreviated member-table column via a hover tooltip", async () => {
const user = userEvent.setup();
render(<ScorecardsTabContent />);
await user.hover(screen.getByText("FPY"));
expect(await screen.findByRole("tooltip")).toHaveTextContent(
/first-pass yield/i,
);
});
it("surfaces load errors instead of an endless skeleton", () => {
mockOrg.mockReturnValue({
data: undefined,
@@ -1,5 +1,6 @@
import { describe, it, expect } from "vitest";
import { render, screen } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import { TaskStatusTiles } from "../task-status-tiles";
describe("TaskStatusTiles", () => {
@@ -34,4 +35,24 @@ describe("TaskStatusTiles", () => {
expect(screen.getByText(label)).toBeInTheDocument();
});
});
it("explains a tile's meaning via a hover tooltip when one is provided", async () => {
const user = userEvent.setup();
render(
<TaskStatusTiles
tiles={[
{
label: "Blocked",
value: 2,
icon: <span>icon</span>,
tip: "Tasks stuck on an external dependency",
},
]}
/>,
);
await user.hover(screen.getByText("Blocked"));
expect(await screen.findByRole("tooltip")).toHaveTextContent(
"external dependency",
);
});
});
@@ -13,6 +13,7 @@ import {
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { Skeleton } from "@/components/ui/skeleton";
import { SegmentedControl } from "@/components/ui/segmented-control";
import { HelpTip } from "@/components/ui/help-tip";
import { useIsMobile } from "@/hooks/use-is-mobile";
import type { AgentUsageRow } from "@/types";
@@ -70,7 +71,11 @@ export function AgentUsageChart({ data, isLoading }: AgentUsageChartProps) {
<tr>
<th className="text-left font-medium py-1">Agent</th>
<th className="text-right font-medium py-1">Tokens</th>
<th className="text-right font-medium py-1">%</th>
<th className="text-right font-medium py-1">
<HelpTip label="Share of total tokens across all agents in this window">
<span>%</span>
</HelpTip>
</th>
</tr>
</thead>
<tbody>
+36 -16
View File
@@ -12,6 +12,7 @@ import {
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { Skeleton } from "@/components/ui/skeleton";
import { Badge } from "@/components/ui/badge";
import { HelpTip } from "@/components/ui/help-tip";
import {
ResponsiveTable,
ResponsiveTableCardList,
@@ -121,25 +122,31 @@ function BottlenecksCard() {
) : (
<>
<div className="flex items-center gap-2 text-sm">
<span className="text-muted-foreground">Worst stage:</span>
<HelpTip label="The status stage that has accumulated the most total time across all tasks in the window">
<span className="text-muted-foreground">Worst stage:</span>
</HelpTip>
{data?.worst_stage ? (
<Badge variant="destructive">{label(data.worst_stage)}</Badge>
) : (
<span className="text-muted-foreground"></span>
)}
<span className="ml-auto text-muted-foreground">
{data?.active_blockers ?? 0} active blockers
</span>
<HelpTip label="Tasks currently in the blocked status right now, across all teams">
<span className="ml-auto text-muted-foreground">
{data?.active_blockers ?? 0} active blockers
</span>
</HelpTip>
</div>
<div className="space-y-2">
{(data?.by_stage ?? []).slice(0, 6).map((s) => (
<div key={s.status} className="space-y-1">
<div className="flex justify-between text-xs">
<span>{label(s.status)}</span>
<span className="text-muted-foreground">
{fmtDuration(s.cumulative_seconds)} · {s.parked_now}{" "}
parked
</span>
<HelpTip label="Cumulative time spent in this stage across all tasks · how many tasks are sitting in it right now">
<span className="text-muted-foreground">
{fmtDuration(s.cumulative_seconds)} · {s.parked_now}{" "}
parked
</span>
</HelpTip>
</div>
<div className="h-2 w-full rounded bg-muted">
<div
@@ -169,7 +176,9 @@ function ReworkCard() {
return (
<Card>
<CardHeader className="pb-2">
<CardTitle className="text-base">Rework (30d)</CardTitle>
<HelpTip label="A completed task 'bounces' when it's sent to needs_revision at least once — by QA, the PR gate, the PM, or the CEO">
<CardTitle className="text-base">Rework (30d)</CardTitle>
</HelpTip>
</CardHeader>
<CardContent className="space-y-4">
{isLoading ? (
@@ -197,7 +206,11 @@ function ReworkCard() {
<thead className="text-muted-foreground">
<tr className="text-left">
<th className="py-1 font-medium">Agent</th>
<th className="py-1 font-medium text-right">Rate</th>
<th className="py-1 font-medium text-right">
<HelpTip label="Share of this agent's completed tasks that bounced back for revision at least once">
<span>Rate</span>
</HelpTip>
</th>
<th className="py-1 font-medium text-right">
QA fails
</th>
@@ -287,11 +300,13 @@ function CellScorecard({ team }: { team: string }) {
}
function ScorecardBody({ card }: { card: Scorecard | undefined }) {
const stat = (k: string, v: string) => (
<div className="flex justify-between text-sm">
<span className="text-muted-foreground">{k}</span>
<span className="font-medium">{v}</span>
</div>
const stat = (k: string, v: string, tip?: string) => (
<HelpTip label={tip}>
<div className="flex justify-between text-sm">
<span className="text-muted-foreground">{k}</span>
<span className="font-medium">{v}</span>
</div>
</HelpTip>
);
return (
<div className="space-y-1">
@@ -301,8 +316,13 @@ function ScorecardBody({ card }: { card: Scorecard | undefined }) {
card?.avg_cycle_hours != null
? card.avg_cycle_hours.toFixed(1) + "h"
: "—",
"Average wall-clock time from claim to completion",
)}
{stat(
"Rework",
pct(card?.rework_rate ?? 0),
"Share of completed tasks that bounced back for revision at least once",
)}
{stat("Rework", pct(card?.rework_rate ?? 0))}
{stat("Cost", "$" + (card?.cost_usd ?? 0).toFixed(2))}
</div>
);
+92 -27
View File
@@ -3,6 +3,7 @@
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { Skeleton } from "@/components/ui/skeleton";
import { Badge } from "@/components/ui/badge";
import { HelpTip } from "@/components/ui/help-tip";
import {
Table,
TableBody,
@@ -85,21 +86,35 @@ function OrgSummary() {
</div>
);
if (isLoading || !data) return <Skeleton className="h-24 w-full" />;
const cells: [string, string][] = [
const cells: [string, string, string?][] = [
["Members", String(data.member_count)],
["Completed", String(data.tasks_completed)],
["First-pass yield", pctOrNa(data.first_pass_yield)],
["Throughput/hr", numOrNa(data.effort_throughput_per_hour, 2)],
["Active effort", data.active_runtime_hours.toFixed(1) + "h"],
[
"First-pass yield",
pctOrNa(data.first_pass_yield),
"Share of completed tasks that shipped without a QA/PR-gate/PM/CEO bounce",
],
[
"Throughput/hr",
numOrNa(data.effort_throughput_per_hour, 2),
"Tasks completed per hour of active (non-idle) agent runtime",
],
[
"Active effort",
data.active_runtime_hours.toFixed(1) + "h",
"Total hours agents spent actively working — idle time excluded",
],
["Cost", "$" + data.cost_usd.toFixed(2)],
];
return (
<div className="grid grid-cols-2 gap-4 sm:grid-cols-3 lg:grid-cols-6">
{cells.map(([k, v]) => (
<div key={k}>
<div className="text-2xl font-semibold">{v}</div>
<div className="text-muted-foreground text-sm">{k}</div>
</div>
{cells.map(([k, v, tip]) => (
<HelpTip key={k} label={tip}>
<div>
<div className="text-2xl font-semibold">{v}</div>
<div className="text-muted-foreground text-sm">{k}</div>
</div>
</HelpTip>
))}
</div>
);
@@ -114,21 +129,39 @@ function CeoCard() {
</div>
);
if (isLoading || !data) return <Skeleton className="h-24 w-full" />;
const cells: [string, string][] = [
const cells: [string, string, string?][] = [
["Approvals", String(data.approval_count)],
["Approval p50", hoursOrDash(data.approval_p50_seconds)],
["Approval p90", hoursOrDash(data.approval_p90_seconds)],
[
"Approval p50",
hoursOrDash(data.approval_p50_seconds),
"Median time from a task reaching your queue to your approval",
],
[
"Approval p90",
hoursOrDash(data.approval_p90_seconds),
"90th percentile — the slowest 10% of approvals took at least this long",
],
["Unblocks", String(data.unblock_count)],
["Unblock p50", hoursOrDash(data.unblock_p50_seconds)],
["God-mode actions", String(data.godmode_actions)],
[
"Unblock p50",
hoursOrDash(data.unblock_p50_seconds),
"Median time from a task blocking to you unblocking it",
],
[
"God-mode actions",
String(data.godmode_actions),
"Direct admin overrides you made outside the normal approval flow",
],
];
return (
<div className="grid grid-cols-2 gap-4 sm:grid-cols-3 lg:grid-cols-6">
{cells.map(([k, v]) => (
<div key={k}>
<div className="text-2xl font-semibold">{v}</div>
<div className="text-muted-foreground text-sm">{k}</div>
</div>
{cells.map(([k, v, tip]) => (
<HelpTip key={k} label={tip}>
<div>
<div className="text-2xl font-semibold">{v}</div>
<div className="text-muted-foreground text-sm">{k}</div>
</div>
</HelpTip>
))}
</div>
);
@@ -168,14 +201,46 @@ export function ScorecardsTabContent() {
<TableHeader>
<TableRow>
<TableHead>Member</TableHead>
<TableHead>Done</TableHead>
<TableHead>FPY</TableHead>
<TableHead>Effort</TableHead>
<TableHead>Turns/task</TableHead>
<TableHead>QA pass</TableHead>
<TableHead>Escal.</TableHead>
<TableHead>Blocked others</TableHead>
<TableHead>Util.</TableHead>
<TableHead>
<HelpTip label="Tasks completed">
<span>Done</span>
</HelpTip>
</TableHead>
<TableHead>
<HelpTip label="First-pass yield — share of completed tasks shipped without a QA/PR-gate/PM/CEO bounce">
<span>FPY</span>
</HelpTip>
</TableHead>
<TableHead>
<HelpTip label="Total hours actively working — idle/waiting time excluded">
<span>Effort</span>
</HelpTip>
</TableHead>
<TableHead>
<HelpTip label="Average number of agent turns spent per completed task">
<span>Turns/task</span>
</HelpTip>
</TableHead>
<TableHead>
<HelpTip label="Share of this agent's QA reviews that passed on the first attempt">
<span>QA pass</span>
</HelpTip>
</TableHead>
<TableHead>
<HelpTip label="Escalations — times this agent's work was escalated up the chain">
<span>Escal.</span>
</HelpTip>
</TableHead>
<TableHead>
<HelpTip label="Times this agent's work blocked another agent's progress">
<span>Blocked others</span>
</HelpTip>
</TableHead>
<TableHead>
<HelpTip label="Utilization — share of this agent's spawned time spent actively working, not idle">
<span>Util.</span>
</HelpTip>
</TableHead>
</TableRow>
</TableHeader>
<TableBody>
@@ -1,12 +1,15 @@
"use client";
import { Card } from "@/components/ui/card";
import { HelpTip } from "@/components/ui/help-tip";
import { cn } from "@/lib/utils";
export interface TaskStatusTileData {
label: string;
value: number;
icon: React.ReactNode;
/** What this status means — shown on hover. Omit for a self-explanatory label. */
tip?: string;
}
interface TaskStatusTilesProps {
@@ -23,13 +26,15 @@ export function TaskStatusTiles({ tiles, className }: TaskStatusTilesProps) {
return (
<div className={cn("grid grid-cols-2 sm:grid-cols-3 gap-2", className)}>
{tiles.map((tile) => (
<Card key={tile.label} className="gap-1 py-3">
<div className="flex items-center gap-1.5 px-3 text-xs text-muted-foreground">
{tile.icon}
<span className="truncate">{tile.label}</span>
</div>
<div className="px-3 text-xl font-bold">{tile.value}</div>
</Card>
<HelpTip key={tile.label} label={tile.tip}>
<Card className="gap-1 py-3">
<div className="flex items-center gap-1.5 px-3 text-xs text-muted-foreground">
{tile.icon}
<span className="truncate">{tile.label}</span>
</div>
<div className="px-3 text-xl font-bold">{tile.value}</div>
</Card>
</HelpTip>
))}
</div>
);
@@ -13,6 +13,7 @@ import {
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { Skeleton } from "@/components/ui/skeleton";
import { SegmentedControl } from "@/components/ui/segmented-control";
import { HelpTip } from "@/components/ui/help-tip";
import { useIsMobile } from "@/hooks/use-is-mobile";
import type { TeamUsageRow } from "@/types";
@@ -67,7 +68,11 @@ export function TeamUsageChart({ data, isLoading }: TeamUsageChartProps) {
<tr>
<th className="text-left font-medium py-1">Team</th>
<th className="text-right font-medium py-1">Tokens</th>
<th className="text-right font-medium py-1">%</th>
<th className="text-right font-medium py-1">
<HelpTip label="Share of total tokens across all teams in this window">
<span>%</span>
</HelpTip>
</th>
</tr>
</thead>
<tbody>