feat(panel): dense tooltip pass — org surfaces + layout chrome (253 tips)

Every sidebar destination, metric derivation, sortable column, queue
action, and dialog control explains itself; mobile tab bar shares the
sidebar's descriptions so the two nav surfaces can't drift.
This commit is contained in:
Renn F
2026-07-15 18:04:00 +02:00
parent e7d8e7ecb5
commit f9dbb30a4e
63 changed files with 1770 additions and 1017 deletions
+30 -24
View File
@@ -310,15 +310,17 @@ function A2APageContent() {
<> <>
{/* Mobile-only back affordance — drills back up to the list. */} {/* Mobile-only back affordance — drills back up to the list. */}
{onDetailLevel && ( {onDetailLevel && (
<Button <HelpTip label="Returns to the switchboard/list">
variant="ghost" <Button
size="sm" variant="ghost"
className="mb-2 w-fit shrink-0 lg:hidden" size="sm"
onClick={handleBack} className="mb-2 w-fit shrink-0 lg:hidden"
> onClick={handleBack}
<ArrowLeft className="h-4 w-4 mr-2" /> >
Back <ArrowLeft className="h-4 w-4 mr-2" />
</Button> Back
</Button>
</HelpTip>
)} )}
<div className="grid flex-1 min-h-0 grid-cols-12 gap-4 lg:gap-6"> <div className="grid flex-1 min-h-0 grid-cols-12 gap-4 lg:gap-6">
@@ -423,21 +425,25 @@ function A2APageContent() {
{" ↔ "} {" ↔ "}
{getAgentDisplayName(selected.agent_b)} {getAgentDisplayName(selected.agent_b)}
</span> </span>
<Badge <HelpTip label={selected.status === "active" ? "Actively exchanging messages" : "No longer active"}>
variant={ <Badge
selected.status === "active" variant={
? "default" selected.status === "active"
: "secondary" ? "default"
} : "secondary"
className="text-xs" }
> className="text-xs w-fit"
{selected.status} >
</Badge> {selected.status}
<span className="text-xs text-muted-foreground ml-auto"> </Badge>
{selected.message_count} msgs · updated{" "} </HelpTip>
{formatDistanceToNow(new Date(selected.updated_at))}{" "} <HelpTip label={new Date(selected.updated_at).toLocaleString()}>
ago <span className="text-xs text-muted-foreground ml-auto w-fit">
</span> {selected.message_count} msgs · updated{" "}
{formatDistanceToNow(new Date(selected.updated_at))}{" "}
ago
</span>
</HelpTip>
</div> </div>
)} )}
{/* Scoped to the stream pane, not a full-page takeover — {/* Scoped to the stream pane, not a full-page takeover —
@@ -19,6 +19,7 @@ import {
} from "@/components/ui/card"; } from "@/components/ui/card";
import { Badge } from "@/components/ui/badge"; import { Badge } from "@/components/ui/badge";
import { Skeleton } from "@/components/ui/skeleton"; import { Skeleton } from "@/components/ui/skeleton";
import { HelpTip } from "@/components/ui/help-tip";
import { import {
ArrowLeft, ArrowLeft,
Play, Play,
@@ -118,10 +119,12 @@ export default function AgentDetailPage() {
if (isInvalidAgent) { if (isInvalidAgent) {
return ( return (
<div className="space-y-6"> <div className="space-y-6">
<Button variant="ghost" onClick={() => router.back()}> <HelpTip label="Returns to the previous page">
<ArrowLeft className="h-4 w-4 mr-2" /> <Button variant="ghost" onClick={() => router.back()}>
Back <ArrowLeft className="h-4 w-4 mr-2" />
</Button> Back
</Button>
</HelpTip>
<Card className="w-full max-w-lg mx-auto"> <Card className="w-full max-w-lg mx-auto">
<CardContent className="pt-6"> <CardContent className="pt-6">
<div className="flex items-center gap-2 text-red-500"> <div className="flex items-center gap-2 text-red-500">
@@ -131,16 +134,20 @@ export default function AgentDetailPage() {
<p className="text-muted-foreground mt-2"> <p className="text-muted-foreground mt-2">
The agent may not be running or the ID is invalid. The agent may not be running or the ID is invalid.
</p> </p>
<SpawnAgentDialog <HelpTip label="Starts a fresh container for this agent so it can pick up work">
agentId={agentId} <span>
agentName={displayName} <SpawnAgentDialog
trigger={ agentId={agentId}
<Button className="mt-4"> agentName={displayName}
<Play className="h-4 w-4 mr-2" /> trigger={
Spawn Agent <Button className="mt-4">
</Button> <Play className="h-4 w-4 mr-2" />
} Spawn Agent
/> </Button>
}
/>
</span>
</HelpTip>
</CardContent> </CardContent>
</Card> </Card>
</div> </div>
@@ -172,47 +179,61 @@ export default function AgentDetailPage() {
{displayName} {displayName}
</h1> </h1>
{roleLabel && ( {roleLabel && (
<Badge variant="secondary" className="gap-1"> <HelpTip label="Fixed org role — set at agent creation, cannot change at runtime">
<User className="h-3 w-3" /> <Badge variant="secondary" className="gap-1">
{roleLabel} <User className="h-3 w-3" />
</Badge> {roleLabel}
</Badge>
</HelpTip>
)} )}
{teamLabel && ( {teamLabel && (
<Badge variant="outline" className="gap-1"> <HelpTip label="Cell/team this agent belongs to in the org hierarchy">
<Users className="h-3 w-3" /> <Badge variant="outline" className="gap-1">
{teamLabel} <Users className="h-3 w-3" />
</Badge> {teamLabel}
</Badge>
</HelpTip>
)} )}
</div> </div>
<p className="text-muted-foreground"> <HelpTip label={agentId !== displayName ? "This agent's slug — used in URLs, branch names, and commits" : ""}>
{agentId !== displayName ? `@${agentId}` : "Agent Details"} <p className="text-muted-foreground w-fit">
</p> {agentId !== displayName ? `@${agentId}` : "Agent Details"}
</p>
</HelpTip>
</div> </div>
</div> </div>
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
{isWaiting && <ResolveWaitDialog agentId={agentId} />} {isWaiting && <ResolveWaitDialog agentId={agentId} />}
{isActive ? ( {isActive ? (
<> <>
<Button variant="outline" onClick={() => handleStop(true)}> <HelpTip label="Lets the agent finish its current step before stopping">
<Square className="h-4 w-4 mr-2" /> <Button variant="outline" onClick={() => handleStop(true)}>
Stop <Square className="h-4 w-4 mr-2" />
</Button> Stop
<Button variant="destructive" onClick={() => handleStop(false)}> </Button>
<Square className="h-4 w-4 mr-2" /> </HelpTip>
Force Stop <HelpTip label="Kills the container immediately, even mid-task">
</Button> <Button variant="destructive" onClick={() => handleStop(false)}>
<Square className="h-4 w-4 mr-2" />
Force Stop
</Button>
</HelpTip>
</> </>
) : ( ) : (
<SpawnAgentDialog <HelpTip label="Starts a fresh container for this agent so it can pick up work">
agentId={agentId} <span>
agentName={displayName} <SpawnAgentDialog
trigger={ agentId={agentId}
<Button> agentName={displayName}
<Play className="h-4 w-4 mr-2" /> trigger={
Spawn <Button>
</Button> <Play className="h-4 w-4 mr-2" />
} Spawn
/> </Button>
}
/>
</span>
</HelpTip>
)} )}
</div> </div>
</div> </div>
@@ -245,16 +266,20 @@ export default function AgentDetailPage() {
Live status is unavailable this agent isn&apos;t currently Live status is unavailable this agent isn&apos;t currently
active. Spawn it to see live state. active. Spawn it to see live state.
</p> </p>
<SpawnAgentDialog <HelpTip label="Starts a fresh container for this agent so it can pick up work">
agentId={agentId} <span>
agentName={displayName} <SpawnAgentDialog
trigger={ agentId={agentId}
<Button className="mt-4"> agentName={displayName}
<Play className="h-4 w-4 mr-2" /> trigger={
Spawn Agent <Button className="mt-4">
</Button> <Play className="h-4 w-4 mr-2" />
} Spawn Agent
/> </Button>
}
/>
</span>
</HelpTip>
</CardContent> </CardContent>
</Card> </Card>
) : agent ? ( ) : agent ? (
@@ -287,9 +312,11 @@ export default function AgentDetailPage() {
</CardTitle> </CardTitle>
</CardHeader> </CardHeader>
<CardContent> <CardContent>
<p className="text-lg font-semibold text-red-600"> <HelpTip label="Errors this agent has hit since its current session started">
{agent.error_count} error(s) <p className="text-lg font-semibold text-red-600 w-fit">
</p> {agent.error_count} error(s)
</p>
</HelpTip>
</CardContent> </CardContent>
</Card> </Card>
)} )}
@@ -108,6 +108,7 @@ export default function AgentsPage() {
one Leadership band so a lone Main PM card never wastes a full row. */} one Leadership band so a lone Main PM card never wastes a full row. */}
<AgentGrid <AgentGrid
title="Leadership" title="Leadership"
titleHint="Board (Product Owner, Head of Marketing, Auditor) plus the Main PM"
agents={[...getBoardAgents(agents), ...getMainPm(agents)]} agents={[...getBoardAgents(agents), ...getMainPm(agents)]}
agentStatuses={agentStatuses} agentStatuses={agentStatuses}
agentUsage={agentUsageMap} agentUsage={agentUsageMap}
@@ -116,6 +117,7 @@ export default function AgentsPage() {
<AgentGrid <AgentGrid
title="Backend Cell" title="Backend Cell"
titleHint="2 Devs, 1 QA, 1 PM, 1 Documenter, 1 PR Reviewer"
agents={getBackendAgents(agents)} agents={getBackendAgents(agents)}
agentStatuses={agentStatuses} agentStatuses={agentStatuses}
agentUsage={agentUsageMap} agentUsage={agentUsageMap}
@@ -124,6 +126,7 @@ export default function AgentsPage() {
<AgentGrid <AgentGrid
title="Frontend Cell" title="Frontend Cell"
titleHint="2 Devs, 1 QA, 1 PM, 1 Documenter, 1 PR Reviewer"
agents={getFrontendAgents(agents)} agents={getFrontendAgents(agents)}
agentStatuses={agentStatuses} agentStatuses={agentStatuses}
agentUsage={agentUsageMap} agentUsage={agentUsageMap}
@@ -132,6 +135,7 @@ export default function AgentsPage() {
<AgentGrid <AgentGrid
title="UX/UI Cell" title="UX/UI Cell"
titleHint="2 Devs, 1 QA, 1 PM, 1 Documenter, 1 PR Reviewer"
agents={getUxAgents(agents)} agents={getUxAgents(agents)}
agentStatuses={agentStatuses} agentStatuses={agentStatuses}
agentUsage={agentUsageMap} agentUsage={agentUsageMap}
@@ -143,6 +147,7 @@ export default function AgentsPage() {
{getSupportAgents(agents).length > 0 && ( {getSupportAgents(agents).length > 0 && (
<AgentGrid <AgentGrid
title="Support" title="Support"
titleHint="CEO-direct helpers: Intake/Prompter, Secretary, and the root PR Reviewer"
agents={getSupportAgents(agents)} agents={getSupportAgents(agents)}
agentStatuses={agentStatuses} agentStatuses={agentStatuses}
agentUsage={agentUsageMap} agentUsage={agentUsageMap}
@@ -129,7 +129,9 @@ export default function JournalEntryPage({ params }: JournalEntryPageProps) {
<div className="flex items-center gap-2 mb-1"> <div className="flex items-center gap-2 mb-1">
<EntryTypeBadge type={entry.type} /> <EntryTypeBadge type={entry.type} />
{entry.sentiment && ( {entry.sentiment && (
<Badge variant="outline">{entry.sentiment}</Badge> <HelpTip label="Agent's self-reported sentiment when writing this entry">
<Badge variant="outline">{entry.sentiment}</Badge>
</HelpTip>
)} )}
</div> </div>
<h1 className="text-2xl font-bold">{entry.title}</h1> <h1 className="text-2xl font-bold">{entry.title}</h1>
@@ -192,7 +194,9 @@ export default function JournalEntryPage({ params }: JournalEntryPageProps) {
{/* Content */} {/* Content */}
<Card> <Card>
<CardHeader> <CardHeader>
<CardTitle>Content</CardTitle> <HelpTip label="The entry body, rendered as Markdown">
<CardTitle>Content</CardTitle>
</HelpTip>
</CardHeader> </CardHeader>
<CardContent> <CardContent>
<div className="prose prose-sm dark:prose-invert max-w-none"> <div className="prose prose-sm dark:prose-invert max-w-none">
@@ -205,10 +209,12 @@ export default function JournalEntryPage({ params }: JournalEntryPageProps) {
{entry.tags.length > 0 && ( {entry.tags.length > 0 && (
<Card> <Card>
<CardHeader> <CardHeader>
<CardTitle className="flex items-center gap-2"> <HelpTip label="Free-form labels the agent attached to this entry">
<Tag className="h-4 w-4" /> <CardTitle className="flex items-center gap-2">
Tags <Tag className="h-4 w-4" />
</CardTitle> Tags
</CardTitle>
</HelpTip>
</CardHeader> </CardHeader>
<CardContent> <CardContent>
<div className="flex flex-wrap gap-2"> <div className="flex flex-wrap gap-2">
+12 -9
View File
@@ -9,6 +9,7 @@ import { JournalView } from "@/components/journals/journal-view";
import { Card, CardContent } from "@/components/ui/card"; import { Card, CardContent } from "@/components/ui/card";
import { Input } from "@/components/ui/input"; import { Input } from "@/components/ui/input";
import { Skeleton } from "@/components/ui/skeleton"; import { Skeleton } from "@/components/ui/skeleton";
import { HelpTip } from "@/components/ui/help-tip";
import { usePageRefresh } from "@/hooks"; import { usePageRefresh } from "@/hooks";
import { BookOpen, Search } from "lucide-react"; import { BookOpen, Search } from "lucide-react";
@@ -179,15 +180,17 @@ function JournalsPageContent() {
<Card className="h-full flex flex-col"> <Card className="h-full flex flex-col">
<CardContent className="p-3 flex flex-1 flex-col min-h-0"> <CardContent className="p-3 flex flex-1 flex-col min-h-0">
{/* Agent Search */} {/* Agent Search */}
<div className="relative mb-3"> <HelpTip label="Filters the agent list below by ID, role, or team">
<Search className="absolute left-3 top-1/2 transform -translate-y-1/2 h-4 w-4 text-muted-foreground" /> <div className="relative mb-3">
<Input <Search className="absolute left-3 top-1/2 transform -translate-y-1/2 h-4 w-4 text-muted-foreground" />
value={agentSearch} <Input
onChange={(e) => handleAgentSearch(e.target.value)} value={agentSearch}
placeholder="Search agents..." onChange={(e) => handleAgentSearch(e.target.value)}
className="pl-9" placeholder="Search agents..."
/> className="pl-9"
</div> />
</div>
</HelpTip>
{/* Agent List */} {/* Agent List */}
<AgentList <AgentList
+69 -22
View File
@@ -26,6 +26,11 @@ import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
import { SegmentedControl } from "@/components/ui/segmented-control"; import { SegmentedControl } from "@/components/ui/segmented-control";
import { OfflineState } from "@/components/ui/offline-state"; import { OfflineState } from "@/components/ui/offline-state";
import { HelpTip } from "@/components/ui/help-tip"; import { HelpTip } from "@/components/ui/help-tip";
import {
Tooltip,
TooltipContent,
TooltipTrigger,
} from "@/components/ui/tooltip";
import { import {
ResponsiveTable, ResponsiveTable,
ResponsiveTableCardList, ResponsiveTableCardList,
@@ -89,6 +94,7 @@ interface MetricCardProps {
icon: React.ReactNode; icon: React.ReactNode;
trend?: "up" | "down" | "neutral"; trend?: "up" | "down" | "neutral";
trendValue?: string; trendValue?: string;
tip?: string;
} }
function MetricCard({ function MetricCard({
@@ -98,16 +104,19 @@ function MetricCard({
icon, icon,
trend, trend,
trendValue, trendValue,
tip,
}: MetricCardProps) { }: MetricCardProps) {
const displayValue = typeof value === "number" ? humanizeCount(value) : value; const displayValue = typeof value === "number" ? humanizeCount(value) : value;
return ( return (
<Card> <Card>
<CardHeader className="flex flex-row items-center justify-between pb-2"> <HelpTip label={tip}>
<CardTitle className="text-sm font-medium text-muted-foreground"> <CardHeader className="flex flex-row items-center justify-between pb-2">
{title} <CardTitle className="text-sm font-medium text-muted-foreground">
</CardTitle> {title}
{icon} </CardTitle>
</CardHeader> {icon}
</CardHeader>
</HelpTip>
<CardContent> <CardContent>
<div className="text-2xl font-bold">{displayValue}</div> <div className="text-2xl font-bold">{displayValue}</div>
{subtitle && ( {subtitle && (
@@ -305,18 +314,21 @@ function PerformanceTabContent() {
value={completedToday} value={completedToday}
subtitle="Tasks finished" subtitle="Tasks finished"
icon={<Zap className="h-4 w-4 text-green-500" />} icon={<Zap className="h-4 w-4 text-green-500" />}
tip="Tasks whose completed_at timestamp falls on today's calendar date"
/> />
<MetricCard <MetricCard
title="Completed This Week" title="Completed This Week"
value={completedThisWeek} value={completedThisWeek}
subtitle="Rolling 7 days" subtitle="Rolling 7 days"
icon={<TrendingUp className="h-4 w-4 text-blue-500" />} icon={<TrendingUp className="h-4 w-4 text-blue-500" />}
tip="Tasks completed in the trailing 7 days from now"
/> />
<MetricCard <MetricCard
title="Total Completed" title="Total Completed"
value={completed} value={completed}
subtitle="All time" subtitle="All time"
icon={<CheckCircle className="h-4 w-4 text-green-500" />} icon={<CheckCircle className="h-4 w-4 text-green-500" />}
tip="Every task that has ever reached the completed status"
/> />
<MetricCard <MetricCard
title="Completion Rate" title="Completion Rate"
@@ -327,6 +339,7 @@ function PerformanceTabContent() {
} }
subtitle="Of all tasks" subtitle="Of all tasks"
icon={<Activity className="h-4 w-4 text-purple-500" />} icon={<Activity className="h-4 w-4 text-purple-500" />}
tip="Completed tasks divided by every task in the table, all-time"
/> />
</div> </div>
</div> </div>
@@ -390,24 +403,28 @@ function PerformanceTabContent() {
value={runningAgents} value={runningAgents}
subtitle="Active agents" subtitle="Active agents"
icon={<Users className="h-4 w-4 text-green-500" />} icon={<Users className="h-4 w-4 text-green-500" />}
tip="Agents currently spawned and actively working a task"
/> />
<MetricCard <MetricCard
title="Idle" title="Idle"
value={idleAgents} value={idleAgents}
subtitle="Available" subtitle="Available"
icon={<Users className="h-4 w-4 text-gray-500" />} icon={<Users className="h-4 w-4 text-gray-500" />}
tip="Agents with no container running — free to be spawned for new work"
/> />
<MetricCard <MetricCard
title="Waiting" title="Waiting"
value={waitingAgents} value={waitingAgents}
subtitle="Needs input" subtitle="Needs input"
icon={<Clock className="h-4 w-4 text-yellow-500" />} icon={<Clock className="h-4 w-4 text-yellow-500" />}
tip="Agents stuck waiting unusually long, possibly needing human attention"
/> />
<MetricCard <MetricCard
title="Errors" title="Errors"
value={errorAgents} value={errorAgents}
subtitle="Failed agents" subtitle="Failed agents"
icon={<XCircle className="h-4 w-4 text-red-500" />} icon={<XCircle className="h-4 w-4 text-red-500" />}
tip="Agents whose last spawn exited in an error state"
/> />
</div> </div>
</div> </div>
@@ -624,12 +641,14 @@ interface ProjectionCardProps {
function ProjectionCard({ projection, isLoading }: ProjectionCardProps) { function ProjectionCard({ projection, isLoading }: ProjectionCardProps) {
return ( return (
<Card> <Card>
<CardHeader className="pb-2"> <HelpTip label="Projected month cost extrapolated from the recent daily-average spend, not a hard budget">
<CardTitle className="text-base flex items-center gap-2"> <CardHeader className="pb-2">
<TrendingUp className="h-4 w-4 text-blue-500" /> <CardTitle className="text-base flex items-center gap-2">
Monthly Projection <TrendingUp className="h-4 w-4 text-blue-500" />
</CardTitle> Monthly Projection
</CardHeader> </CardTitle>
</CardHeader>
</HelpTip>
<CardContent> <CardContent>
{isLoading ? ( {isLoading ? (
<Skeleton className="h-10 w-full" /> <Skeleton className="h-10 w-full" />
@@ -699,12 +718,14 @@ interface RoleUsageTableProps {
function RoleUsageTable({ data, isLoading }: RoleUsageTableProps) { function RoleUsageTable({ data, isLoading }: RoleUsageTableProps) {
return ( return (
<Card> <Card>
<CardHeader className="pb-2"> <HelpTip label="Spend and cache-hit rate broken down by agent role (be-dev, qa, main_pm, ...) in the selected window">
<CardTitle className="text-base flex items-center gap-2"> <CardHeader className="pb-2">
<Users className="h-4 w-4 text-blue-500" /> <CardTitle className="text-base flex items-center gap-2">
Cost &amp; Cache by Role <Users className="h-4 w-4 text-blue-500" />
</CardTitle> Cost &amp; Cache by Role
</CardHeader> </CardTitle>
</CardHeader>
</HelpTip>
<CardContent> <CardContent>
{isLoading ? ( {isLoading ? (
<Skeleton className="h-40 w-full" /> <Skeleton className="h-40 w-full" />
@@ -869,6 +890,13 @@ const VALID_METRICS_TABS: MetricsTab[] = [
"scorecards", "scorecards",
]; ];
const METRICS_TAB_HINTS: Record<MetricsTab, string> = {
performance: "Task velocity, status counts, agent load, and team health",
"token-usage": "Token spend, cost projections, cache efficiency, and per-session detail",
delivery: "Cycle time, bottlenecks, and rework rate reconstructed from the audit log",
scorecards: "Per-agent and per-team delivery scorecards",
};
function isValidMetricsTab(value: string | null): value is MetricsTab { function isValidMetricsTab(value: string | null): value is MetricsTab {
return VALID_METRICS_TABS.includes(value as MetricsTab); return VALID_METRICS_TABS.includes(value as MetricsTab);
} }
@@ -905,10 +933,29 @@ function MetricsPageContent() {
<Tabs value={activeTab} onValueChange={handleTabChange}> <Tabs value={activeTab} onValueChange={handleTabChange}>
<TabsList> <TabsList>
<TabsTrigger value="performance">Performance</TabsTrigger> {VALID_METRICS_TABS.map((tab) => (
<TabsTrigger value="token-usage">Token Usage</TabsTrigger> <Tooltip key={tab}>
<TabsTrigger value="delivery">Delivery</TabsTrigger> <TooltipTrigger asChild>
<TabsTrigger value="scorecards">Scorecards</TabsTrigger> {/* TooltipTrigger's asChild Slot merge overwrites the
data-state Radix Tabs sets on this trigger, so the
data-[state=active] style never fires unless it's
re-asserted explicitly after the merge. */}
<TabsTrigger
value={tab}
data-state={tab === activeTab ? "active" : "inactive"}
>
{tab === "performance"
? "Performance"
: tab === "token-usage"
? "Token Usage"
: tab === "delivery"
? "Delivery"
: "Scorecards"}
</TabsTrigger>
</TooltipTrigger>
<TooltipContent>{METRICS_TAB_HINTS[tab]}</TooltipContent>
</Tooltip>
))}
</TabsList> </TabsList>
<TabsContent value="performance" className="mt-6"> <TabsContent value="performance" className="mt-6">
@@ -12,20 +12,22 @@ import { connectionDotClasses, connectionStateLabel } from "./a2a-utils";
* conversation must read differently from a stream that is the problem. */ * conversation must read differently from a stream that is the problem. */
export function A2AConnectionBadge({ state }: { state: ConnectionState }) { export function A2AConnectionBadge({ state }: { state: ConnectionState }) {
return ( return (
<div className="flex items-center gap-1.5"> <HelpTip label="Live /ws/system connection status for this A2A view — auto-reconnects on drop">
<span <div className="flex items-center gap-1.5 w-fit">
className={cn("h-2 w-2 rounded-full", connectionDotClasses(state))} <span
/> className={cn("h-2 w-2 rounded-full", connectionDotClasses(state))}
<span className="text-xs text-muted-foreground"> />
{connectionStateLabel(state)} <span className="text-xs text-muted-foreground">
</span> {connectionStateLabel(state)}
{(state === "connecting" || state === "reconnecting") && ( </span>
<Loader2 className="h-3 w-3 animate-spin text-muted-foreground" /> {(state === "connecting" || state === "reconnecting") && (
)} <Loader2 className="h-3 w-3 animate-spin text-muted-foreground" />
{state === "disconnected" && ( )}
<WifiOff className="h-3 w-3 text-muted-foreground" /> {state === "disconnected" && (
)} <WifiOff className="h-3 w-3 text-muted-foreground" />
</div> )}
</div>
</HelpTip>
); );
} }
+32 -24
View File
@@ -40,12 +40,14 @@ function IdentityCard({ slug }: { slug: string }) {
<div className="text-sm font-medium truncate"> <div className="text-sm font-medium truncate">
{getAgentDisplayName(slug)} {getAgentDisplayName(slug)}
</div> </div>
<Badge <HelpTip label="Team this agent belongs to in the org hierarchy">
variant="outline" <Badge
className={cn("text-[10px] mt-0.5", TEAM_COLOR_CLASSES[teamColor])} variant="outline"
> className={cn("text-[10px] mt-0.5 w-fit", TEAM_COLOR_CLASSES[teamColor])}
{teamColor.replace("_", "/")} >
</Badge> {teamColor.replace("_", "/")}
</Badge>
</HelpTip>
</div> </div>
</Link> </Link>
); );
@@ -70,10 +72,12 @@ export function A2AContextPane({
return ( return (
<div className="p-3 space-y-4"> <div className="p-3 space-y-4">
<div className="flex items-center gap-2 pb-2 border-b"> <HelpTip label="Read-only: participant identities and the conversation's linked task">
<Users className="h-4 w-4 text-muted-foreground" /> <div className="flex items-center gap-2 pb-2 border-b w-fit">
<span className="text-sm font-medium">Context</span> <Users className="h-4 w-4 text-muted-foreground" />
</div> <span className="text-sm font-medium">Context</span>
</div>
</HelpTip>
<div className="space-y-2"> <div className="space-y-2">
<IdentityCard slug={agentA} /> <IdentityCard slug={agentA} />
@@ -94,20 +98,24 @@ export function A2AContextPane({
<div className="rounded-lg border p-2.5 space-y-1.5"> <div className="rounded-lg border p-2.5 space-y-1.5">
<div className="text-sm font-medium truncate">{task.title}</div> <div className="text-sm font-medium truncate">{task.title}</div>
<div className="flex items-center justify-between gap-2"> <div className="flex items-center justify-between gap-2">
<Badge <HelpTip label="Current lifecycle status of the linked task">
variant={task.status === "completed" ? "default" : "secondary"} <Badge
className="text-xs" variant={task.status === "completed" ? "default" : "secondary"}
> className="text-xs w-fit"
{task.status} >
</Badge> {task.status}
<Link </Badge>
prefetch={false} </HelpTip>
href={`/tasks/${taskId}`} <HelpTip label="Opens this task's full detail page">
className="inline-flex items-center gap-1 text-xs text-primary hover:underline" <Link
> prefetch={false}
<ListTodo className="h-3 w-3" /> href={`/tasks/${taskId}`}
View task className="inline-flex items-center gap-1 text-xs text-primary hover:underline"
</Link> >
<ListTodo className="h-3 w-3" />
View task
</Link>
</HelpTip>
</div> </div>
</div> </div>
)} )}
@@ -4,6 +4,7 @@ import Link from "next/link";
import { Badge } from "@/components/ui/badge"; import { Badge } from "@/components/ui/badge";
import { Skeleton } from "@/components/ui/skeleton"; import { Skeleton } from "@/components/ui/skeleton";
import { ScrollArea } from "@/components/ui/scroll-area"; import { ScrollArea } from "@/components/ui/scroll-area";
import { HelpTip } from "@/components/ui/help-tip";
import { getAgentDisplayName } from "@/lib/agent-utils"; import { getAgentDisplayName } from "@/lib/agent-utils";
import type { AdminConversationSummary } from "@/lib/api/a2a"; import type { AdminConversationSummary } from "@/lib/api/a2a";
import { usePulseFlash } from "@/hooks/use-pulse-flash"; import { usePulseFlash } from "@/hooks/use-pulse-flash";
@@ -81,42 +82,54 @@ function ConversationRow({
{conversation.topic} {conversation.topic}
</div> </div>
)} )}
<div className="text-xs text-muted-foreground mt-1"> <HelpTip
{formatDistanceToNow( label={new Date(
new Date( conversation.last_message_at ?? conversation.created_at,
conversation.last_message_at ?? conversation.created_at, ).toLocaleString()}
), >
)}{" "} <div className="text-xs text-muted-foreground mt-1 w-fit">
ago {formatDistanceToNow(
</div> new Date(
conversation.last_message_at ?? conversation.created_at,
),
)}{" "}
ago
</div>
</HelpTip>
{conversation.last_message_preview && ( {conversation.last_message_preview && (
<p className="text-xs text-muted-foreground truncate mt-1"> <p className="text-xs text-muted-foreground truncate mt-1">
{conversation.last_message_preview} {conversation.last_message_preview}
</p> </p>
)} )}
{conversation.task_id && ( {conversation.task_id && (
<Link <HelpTip label="Opens the task this conversation is scoped to">
prefetch={false} <Link
href={`/tasks/${conversation.task_id}`} prefetch={false}
onClick={(e) => e.stopPropagation()} href={`/tasks/${conversation.task_id}`}
className="inline-flex items-center gap-1 text-xs text-primary hover:underline mt-1" onClick={(e) => e.stopPropagation()}
> className="inline-flex items-center gap-1 text-xs text-primary hover:underline mt-1"
<ListTodo className="h-3 w-3" /> >
Task {conversation.task_id.slice(0, 8)} <ListTodo className="h-3 w-3" />
</Link> Task {conversation.task_id.slice(0, 8)}
</Link>
</HelpTip>
)} )}
</div> </div>
</div> </div>
<div className="flex flex-col items-end gap-1 shrink-0"> <div className="flex flex-col items-end gap-1 shrink-0">
<Badge <HelpTip label={conversation.status === "active" ? "Actively exchanging messages" : "No longer active"}>
variant={conversation.status === "active" ? "default" : "secondary"} <Badge
className="text-xs" variant={conversation.status === "active" ? "default" : "secondary"}
> className="text-xs w-fit"
{conversation.status} >
</Badge> {conversation.status}
<span className="text-xs text-muted-foreground"> </Badge>
{conversation.message_count} msgs </HelpTip>
</span> <HelpTip label="Total messages exchanged in this conversation">
<span className="text-xs text-muted-foreground w-fit">
{conversation.message_count} msgs
</span>
</HelpTip>
</div> </div>
</div> </div>
</div> </div>
+95 -73
View File
@@ -10,6 +10,7 @@ import {
PopoverTrigger, PopoverTrigger,
} from "@/components/ui/popover"; } from "@/components/ui/popover";
import { SlidersHorizontal, X } from "lucide-react"; import { SlidersHorizontal, X } from "lucide-react";
import { HelpTip } from "@/components/ui/help-tip";
import { getAgentDisplayName } from "@/lib/agent-utils"; import { getAgentDisplayName } from "@/lib/agent-utils";
import { import {
EMPTY_A2A_FILTERS, EMPTY_A2A_FILTERS,
@@ -128,17 +129,19 @@ export function A2AFilterBar({
<div className="mb-2 shrink-0"> <div className="mb-2 shrink-0">
<div className="flex items-center justify-end"> <div className="flex items-center justify-end">
<Popover> <Popover>
<PopoverTrigger asChild> <HelpTip label={count > 0 ? `${count} active filter${count === 1 ? "" : "s"} — click to edit` : "Narrow by agent, task, status, or date"}>
<Button <PopoverTrigger asChild>
type="button" <Button
variant="outline" type="button"
size="sm" variant="outline"
className="h-7 gap-1 px-2 text-xs" size="sm"
> className="h-7 gap-1 px-2 text-xs"
<SlidersHorizontal className="h-3.5 w-3.5" /> >
{count > 0 ? `Filters · ${count}` : "Filters"} <SlidersHorizontal className="h-3.5 w-3.5" />
</Button> {count > 0 ? `Filters · ${count}` : "Filters"}
</PopoverTrigger> </Button>
</PopoverTrigger>
</HelpTip>
<PopoverContent <PopoverContent
align="end" align="end"
className="w-72 max-h-[70vh] space-y-3 overflow-y-auto" className="w-72 max-h-[70vh] space-y-3 overflow-y-auto"
@@ -152,16 +155,20 @@ export function A2AFilterBar({
<div> <div>
<div className="mb-1 flex items-center justify-between"> <div className="mb-1 flex items-center justify-between">
<span className="text-sm font-medium">Agent</span> <HelpTip label="Matches when either participant is one of the checked agents">
<span className="text-sm font-medium w-fit">Agent</span>
</HelpTip>
{filters.agents.length > 0 && ( {filters.agents.length > 0 && (
<Button <HelpTip label="Unchecks every selected agent">
variant="ghost" <Button
size="sm" variant="ghost"
className="h-6 px-2 text-xs" size="sm"
onClick={() => onFiltersChange({ ...filters, agents: [] })} className="h-6 px-2 text-xs"
> onClick={() => onFiltersChange({ ...filters, agents: [] })}
Clear >
</Button> Clear
</Button>
</HelpTip>
)} )}
</div> </div>
<div className="max-h-40 space-y-1 overflow-y-auto"> <div className="max-h-40 space-y-1 overflow-y-auto">
@@ -183,7 +190,9 @@ export function A2AFilterBar({
</div> </div>
<div className="border-t pt-2"> <div className="border-t pt-2">
<span className="text-sm font-medium">Task</span> <HelpTip label="Case-insensitive substring match against the conversation's task id">
<span className="text-sm font-medium w-fit">Task</span>
</HelpTip>
<Input <Input
value={filters.taskIdFragment} value={filters.taskIdFragment}
onChange={(e) => onChange={(e) =>
@@ -196,45 +205,52 @@ export function A2AFilterBar({
className="mt-1 h-7 text-xs" className="mt-1 h-7 text-xs"
aria-label="Task id fragment" aria-label="Task id fragment"
/> />
<label className="mt-2 flex cursor-pointer items-center gap-2"> <HelpTip label="Also shows conversations with no linked task (combined with the fragment above using OR, not AND)">
<Checkbox <label className="mt-2 flex w-fit cursor-pointer items-center gap-2">
checked={filters.noLinkedTask} <Checkbox
onCheckedChange={(checked) => checked={filters.noLinkedTask}
onFiltersChange({ onCheckedChange={(checked) =>
...filters, onFiltersChange({
noLinkedTask: checked === true, ...filters,
}) noLinkedTask: checked === true,
} })
/> }
<span className="text-sm">No linked task</span> />
</label> <span className="text-sm">No linked task</span>
</label>
</HelpTip>
</div> </div>
<div className="border-t pt-2"> <div className="border-t pt-2">
<span className="text-sm font-medium">Status</span> <HelpTip label="List view only — switchboard pairs have no status to filter on">
<span className="text-sm font-medium w-fit">Status</span>
</HelpTip>
<div className="mt-1 flex items-center gap-1"> <div className="mt-1 flex items-center gap-1">
{STATUS_OPTIONS.map((opt) => ( {STATUS_OPTIONS.map((opt) => (
<Button <HelpTip key={opt.value} label={`Toggle showing ${opt.label.toLowerCase()} conversations`}>
key={opt.value} <Button
type="button" type="button"
variant={ variant={
filters.statuses.includes(opt.value) filters.statuses.includes(opt.value)
? "secondary" ? "secondary"
: "outline" : "outline"
} }
size="sm" size="sm"
className="h-7 px-2 text-xs" className="h-7 px-2 text-xs"
aria-pressed={filters.statuses.includes(opt.value)} aria-pressed={filters.statuses.includes(opt.value)}
onClick={() => toggleStatus(opt.value)} onClick={() => toggleStatus(opt.value)}
> >
{opt.label} {opt.label}
</Button> </Button>
</HelpTip>
))} ))}
</div> </div>
</div> </div>
<div className="border-t pt-2"> <div className="border-t pt-2">
<span className="text-sm font-medium">Date range</span> <HelpTip label="Inclusive day-granularity bounds on the conversation's last activity">
<span className="text-sm font-medium w-fit">Date range</span>
</HelpTip>
<div className="mt-1 flex items-center gap-2"> <div className="mt-1 flex items-center gap-2">
<Input <Input
type="date" type="date"
@@ -259,14 +275,16 @@ export function A2AFilterBar({
{count > 0 && ( {count > 0 && (
<div className="flex justify-end border-t pt-2"> <div className="flex justify-end border-t pt-2">
<Button <HelpTip label="Resets every filter dimension back to empty">
variant="ghost" <Button
size="sm" variant="ghost"
className="h-7 px-2 text-xs" size="sm"
onClick={clearAll} className="h-7 px-2 text-xs"
> onClick={clearAll}
Clear all >
</Button> Clear all
</Button>
</HelpTip>
</div> </div>
)} )}
</PopoverContent> </PopoverContent>
@@ -278,23 +296,27 @@ export function A2AFilterBar({
{chips.map((chip) => ( {chips.map((chip) => (
<Badge key={chip.key} variant="secondary" className="gap-1"> <Badge key={chip.key} variant="secondary" className="gap-1">
{chip.label} {chip.label}
<button <HelpTip label="Remove this filter">
type="button" <button
aria-label={`Remove ${chip.label} filter`} type="button"
onClick={chip.onRemove} aria-label={`Remove ${chip.label} filter`}
> onClick={chip.onRemove}
<X className="h-3 w-3 cursor-pointer hover:text-destructive" /> >
</button> <X className="h-3 w-3 cursor-pointer hover:text-destructive" />
</button>
</HelpTip>
</Badge> </Badge>
))} ))}
<Button <HelpTip label="Resets every filter dimension back to empty">
variant="ghost" <Button
size="sm" variant="ghost"
className="h-6 px-2 text-xs" size="sm"
onClick={clearAll} className="h-6 px-2 text-xs"
> onClick={clearAll}
Clear all >
</Button> Clear all
</Button>
</HelpTip>
</div> </div>
)} )}
</div> </div>
+13 -5
View File
@@ -89,11 +89,19 @@ export function A2APairCard({
{" ↔ "} {" ↔ "}
{getAgentDisplayName(pair.agent_b)} {getAgentDisplayName(pair.agent_b)}
</div> </div>
<div className="text-xs text-muted-foreground"> <HelpTip
{hasHistory && pair.last_message_at label={
? `${formatDistanceToNow(new Date(pair.last_message_at))} ago` hasHistory && pair.last_message_at
: "No A2A yet"} ? new Date(pair.last_message_at).toLocaleString()
</div> : "These two agents have never exchanged an A2A message"
}
>
<div className="text-xs text-muted-foreground w-fit">
{hasHistory && pair.last_message_at
? `${formatDistanceToNow(new Date(pair.last_message_at))} ago`
: "No A2A yet"}
</div>
</HelpTip>
</div> </div>
{hasHistory && ( {hasHistory && (
<HelpTip label="Total messages exchanged in this conversation"> <HelpTip label="Total messages exchanged in this conversation">
+18 -11
View File
@@ -12,6 +12,7 @@ import {
} from "@/components/ui/select"; } from "@/components/ui/select";
import { Send } from "lucide-react"; import { Send } from "lucide-react";
import { toast } from "sonner"; import { toast } from "sonner";
import { HelpTip } from "@/components/ui/help-tip";
import { getAgentDisplayName } from "@/lib/agent-utils"; import { getAgentDisplayName } from "@/lib/agent-utils";
import { getErrorMessage } from "@/lib/api/client"; import { getErrorMessage } from "@/lib/api/client";
import { useReplyAsCeo } from "@/hooks/use-a2a-live"; import { useReplyAsCeo } from "@/hooks/use-a2a-live";
@@ -88,9 +89,11 @@ export function A2AReplyComposer({
</div> </div>
<div className="flex flex-col gap-2"> <div className="flex flex-col gap-2">
<Select value={recipient} onValueChange={setChosenRecipient}> <Select value={recipient} onValueChange={setChosenRecipient}>
<SelectTrigger className="w-auto min-w-32 h-8"> <HelpTip label="Defaults to whoever sent the latest message — pick to override">
<SelectValue /> <SelectTrigger className="w-auto min-w-32 h-8">
</SelectTrigger> <SelectValue />
</SelectTrigger>
</HelpTip>
<SelectContent> <SelectContent>
{options.map((slug) => ( {options.map((slug) => (
<SelectItem key={slug} value={slug}> <SelectItem key={slug} value={slug}>
@@ -99,14 +102,18 @@ export function A2AReplyComposer({
))} ))}
</SelectContent> </SelectContent>
</Select> </Select>
<Button <HelpTip label="Sends as the CEO, visible to both participants">
type="submit" <span>
size="sm" <Button
disabled={!content.trim() || disabled || reply.isPending} type="submit"
> size="sm"
<Send className="h-4 w-4 mr-1" /> disabled={!content.trim() || disabled || reply.isPending}
Send >
</Button> <Send className="h-4 w-4 mr-1" />
Send
</Button>
</span>
</HelpTip>
</div> </div>
</div> </div>
<p className="text-xs text-muted-foreground mt-2"> <p className="text-xs text-muted-foreground mt-2">
+9 -6
View File
@@ -2,6 +2,7 @@
import { Radio } from "lucide-react"; import { Radio } from "lucide-react";
import { Skeleton } from "@/components/ui/skeleton"; import { Skeleton } from "@/components/ui/skeleton";
import { HelpTip } from "@/components/ui/help-tip";
import type { AdminPairSummary } from "@/lib/api/a2a"; import type { AdminPairSummary } from "@/lib/api/a2a";
import { A2APairCard } from "./a2a-pair-card"; import { A2APairCard } from "./a2a-pair-card";
import { groupPairsBySection, pairKey } from "./a2a-switchboard-utils"; import { groupPairsBySection, pairKey } from "./a2a-switchboard-utils";
@@ -56,12 +57,14 @@ export function A2ASwitchboard({
<div className="h-full overflow-y-auto p-2 space-y-4"> <div className="h-full overflow-y-auto p-2 space-y-4">
{sections.map((section) => ( {sections.map((section) => (
<div key={section.groupKey}> <div key={section.groupKey}>
<h3 className="text-xs font-semibold uppercase tracking-wide text-muted-foreground mb-2 px-1"> <HelpTip label="Pairs with prior conversation history are listed first">
{section.label} <h3 className="text-xs font-semibold uppercase tracking-wide text-muted-foreground mb-2 px-1 w-fit">
<span className="ml-1.5 text-muted-foreground/60 normal-case"> {section.label}
({section.pairs.length}) <span className="ml-1.5 text-muted-foreground/60 normal-case">
</span> ({section.pairs.length})
</h3> </span>
</h3>
</HelpTip>
<div className="grid grid-cols-1 sm:grid-cols-2 gap-2"> <div className="grid grid-cols-1 sm:grid-cols-2 gap-2">
{section.pairs.map((pair) => { {section.pairs.map((pair) => {
const key = pairKey(pair.agent_a, pair.agent_b); const key = pairKey(pair.agent_a, pair.agent_b);
+30 -22
View File
@@ -156,9 +156,11 @@ export function A2ATranscript({
<AlertTriangle className="h-8 w-8 mx-auto mb-2 opacity-50 text-destructive" /> <AlertTriangle className="h-8 w-8 mx-auto mb-2 opacity-50 text-destructive" />
<p className="text-sm mb-3">Couldn&apos;t load this conversation</p> <p className="text-sm mb-3">Couldn&apos;t load this conversation</p>
{onRetry && ( {onRetry && (
<Button variant="outline" size="sm" onClick={onRetry}> <HelpTip label="Re-fetches this conversation's messages">
Retry <Button variant="outline" size="sm" onClick={onRetry}>
</Button> Retry
</Button>
</HelpTip>
)} )}
</div> </div>
</div> </div>
@@ -224,9 +226,11 @@ export function A2ATranscript({
</div> </div>
<div className="flex-1 min-w-0"> <div className="flex-1 min-w-0">
<div className="flex items-center gap-2 mb-1.5"> <div className="flex items-center gap-2 mb-1.5">
<span className="font-semibold text-sm"> <HelpTip label={`${teamColor.replace("_", "/")} team`}>
{getAgentDisplayName(message.from_agent)} <span className="font-semibold text-sm w-fit">
</span> {getAgentDisplayName(message.from_agent)}
</span>
</HelpTip>
{message.message_kind && ( {message.message_kind && (
<HelpTip label="Type of agent-to-agent message"> <HelpTip label="Type of agent-to-agent message">
<Badge variant="outline" className="text-[10px]"> <Badge variant="outline" className="text-[10px]">
@@ -234,9 +238,11 @@ export function A2ATranscript({
</Badge> </Badge>
</HelpTip> </HelpTip>
)} )}
<span className="text-xs text-muted-foreground ml-auto"> <HelpTip label={new Date(message.created_at).toLocaleString()}>
{formatDistanceToNow(new Date(message.created_at))} ago <span className="text-xs text-muted-foreground ml-auto w-fit">
</span> {formatDistanceToNow(new Date(message.created_at))} ago
</span>
</HelpTip>
</div> </div>
<div className="text-sm prose prose-sm dark:prose-invert max-w-none"> <div className="text-sm prose prose-sm dark:prose-invert max-w-none">
<Markdown>{message.content}</Markdown> <Markdown>{message.content}</Markdown>
@@ -248,19 +254,21 @@ export function A2ATranscript({
</div> </div>
</div> </div>
{showJumpPill && ( {showJumpPill && (
<button <HelpTip label="Scrolls down to the newest message">
type="button" <button
onClick={scrollToBottom} type="button"
className={cn( onClick={scrollToBottom}
"absolute bottom-3 left-1/2 rounded-full bg-primary text-primary-foreground text-xs px-3 py-1 shadow-md", className={cn(
"transition-[opacity,transform] duration-200 ease-out motion-reduce:transition-none", "absolute bottom-3 left-1/2 rounded-full bg-primary text-primary-foreground text-xs px-3 py-1 shadow-md",
pillEntering "transition-[opacity,transform] duration-200 ease-out motion-reduce:transition-none",
? "opacity-0 translate-x-[-50%] translate-y-1 motion-reduce:opacity-100 motion-reduce:translate-y-0" pillEntering
: "opacity-100 translate-x-[-50%] translate-y-0", ? "opacity-0 translate-x-[-50%] translate-y-1 motion-reduce:opacity-100 motion-reduce:translate-y-0"
)} : "opacity-100 translate-x-[-50%] translate-y-0",
> )}
New messages >
</button> New messages
</button>
</HelpTip>
)} )}
</div> </div>
); );
@@ -16,6 +16,7 @@ import {
CardDescription, CardDescription,
} from "@/components/ui/card"; } from "@/components/ui/card";
import { Skeleton } from "@/components/ui/skeleton"; import { Skeleton } from "@/components/ui/skeleton";
import { HelpTip } from "@/components/ui/help-tip";
import { GitBranch, BookOpen } from "lucide-react"; import { GitBranch, BookOpen } from "lucide-react";
import { useUsageTimeSeries } from "@/hooks/use-usage"; import { useUsageTimeSeries } from "@/hooks/use-usage";
import { useWorkSessions } from "@/hooks/use-work-sessions"; import { useWorkSessions } from "@/hooks/use-work-sessions";
@@ -98,7 +99,9 @@ export function AgentActivityPanel({
{/* Token activity sparkline */} {/* Token activity sparkline */}
<Card> <Card>
<CardHeader className="pb-2"> <CardHeader className="pb-2">
<CardTitle className="text-base">Token Activity</CardTitle> <HelpTip label="Daily token usage for this agent's sessions, from the usage rollup">
<CardTitle className="text-base w-fit">Token Activity</CardTitle>
</HelpTip>
<CardDescription>Last 7 days</CardDescription> <CardDescription>Last 7 days</CardDescription>
</CardHeader> </CardHeader>
<CardContent> <CardContent>
@@ -153,7 +156,9 @@ export function AgentActivityPanel({
{/* Activity timeline */} {/* Activity timeline */}
<Card> <Card>
<CardHeader className="pb-2"> <CardHeader className="pb-2">
<CardTitle className="text-base">Recent Activity</CardTitle> <HelpTip label="Latest 8 work sessions and journal entries for this agent, merged and sorted newest first">
<CardTitle className="text-base w-fit">Recent Activity</CardTitle>
</HelpTip>
<CardDescription>Work sessions &amp; journal entries</CardDescription> <CardDescription>Work sessions &amp; journal entries</CardDescription>
</CardHeader> </CardHeader>
<CardContent> <CardContent>
@@ -172,11 +177,19 @@ export function AgentActivityPanel({
{timeline.map((item, i) => ( {timeline.map((item, i) => (
<li key={`${item.kind}-${i}`} className="flex gap-3"> <li key={`${item.kind}-${i}`} className="flex gap-3">
<div className="mt-0.5 shrink-0"> <div className="mt-0.5 shrink-0">
{item.kind === "session" ? ( <HelpTip
<GitBranch className="h-4 w-4 text-muted-foreground" /> label={
) : ( item.kind === "session"
<BookOpen className="h-4 w-4 text-muted-foreground" /> ? "Work session — git branch/task activity"
)} : "Journal entry — agent's own log"
}
>
{item.kind === "session" ? (
<GitBranch className="h-4 w-4 text-muted-foreground" />
) : (
<BookOpen className="h-4 w-4 text-muted-foreground" />
)}
</HelpTip>
</div> </div>
<div className="min-w-0 flex-1"> <div className="min-w-0 flex-1">
<p className="text-sm font-medium truncate">{item.title}</p> <p className="text-sm font-medium truncate">{item.title}</p>
@@ -184,11 +197,13 @@ export function AgentActivityPanel({
{item.subtitle} {item.subtitle}
</p> </p>
</div> </div>
<span className="text-muted-foreground text-xs whitespace-nowrap"> <HelpTip label={new Date(item.timestamp).toLocaleString()}>
{formatDistanceToNow(new Date(item.timestamp), { <span className="text-muted-foreground text-xs whitespace-nowrap">
addSuffix: true, {formatDistanceToNow(new Date(item.timestamp), {
})} addSuffix: true,
</span> })}
</span>
</HelpTip>
</li> </li>
))} ))}
</ol> </ol>
+44 -26
View File
@@ -98,24 +98,30 @@ export function AgentCard({ agent, agentStatus, usageRow }: AgentCardProps) {
)} )}
{isActive && ( {isActive && (
<> <>
<DropdownMenuItem asChild> <HelpTip label="Open this agent's status, activity, and live output stream" side="left">
<Link href={"/agents/" + agent.id} prefetch={false}> <DropdownMenuItem asChild>
<Activity className="h-4 w-4 mr-2" /> <Link href={"/agents/" + agent.id} prefetch={false}>
View Details <Activity className="h-4 w-4 mr-2" />
</Link> View Details
</DropdownMenuItem> </Link>
</DropdownMenuItem>
</HelpTip>
<DropdownMenuSeparator /> <DropdownMenuSeparator />
<DropdownMenuItem onClick={() => handleStop(true)}> <HelpTip label="Lets the agent finish its current step before stopping" side="left">
<Square className="h-4 w-4 mr-2" /> <DropdownMenuItem onClick={() => handleStop(true)}>
Stop Gracefully <Square className="h-4 w-4 mr-2" />
</DropdownMenuItem> Stop Gracefully
<DropdownMenuItem </DropdownMenuItem>
onClick={() => handleStop(false)} </HelpTip>
className="text-red-600" <HelpTip label="Kills the container immediately, even mid-task" side="left">
> <DropdownMenuItem
<Square className="h-4 w-4 mr-2" /> onClick={() => handleStop(false)}
Force Stop className="text-red-600"
</DropdownMenuItem> >
<Square className="h-4 w-4 mr-2" />
Force Stop
</DropdownMenuItem>
</HelpTip>
</> </>
)} )}
</DropdownMenuContent> </DropdownMenuContent>
@@ -139,17 +145,29 @@ export function AgentCard({ agent, agentStatus, usageRow }: AgentCardProps) {
</span> </span>
</HelpTip> </HelpTip>
{detail && ( {detail && (
<p className={cn("mt-1 truncate text-xs", detail.className)}> <HelpTip
{detail.text} label={
</p> agentStatus?.error_count
? "Errors this agent hit in its current session"
: agentStatus?.waiting_for
? "What this agent is blocked on — needs human input to continue"
: "The task this agent currently has claimed"
}
>
<p className={cn("mt-1 truncate text-xs w-fit", detail.className)}>
{detail.text}
</p>
</HelpTip>
)} )}
{usageRow && ( {usageRow && (
<p className="mt-1 truncate text-xs text-muted-foreground"> <HelpTip label="Token usage and cost for this agent over the last 24 hours">
{usageRow.total_tokens >= 1_000 <p className="mt-1 truncate text-xs text-muted-foreground w-fit">
? (usageRow.total_tokens / 1_000).toFixed(1) + "K" {usageRow.total_tokens >= 1_000
: String(usageRow.total_tokens)}{" "} ? (usageRow.total_tokens / 1_000).toFixed(1) + "K"
tok · ${usageRow.cost_usd.toFixed(4)} : String(usageRow.total_tokens)}{" "}
</p> tok · ${usageRow.cost_usd.toFixed(4)}
</p>
</HelpTip>
)} )}
</CardContent> </CardContent>
</Card> </Card>
+15 -6
View File
@@ -3,10 +3,14 @@ import { AgentDefinition } from "@/lib/agent-definitions";
import { Badge } from "@/components/ui/badge"; import { Badge } from "@/components/ui/badge";
import { Card, CardHeader } from "@/components/ui/card"; import { Card, CardHeader } from "@/components/ui/card";
import { Skeleton } from "@/components/ui/skeleton"; import { Skeleton } from "@/components/ui/skeleton";
import { HelpTip } from "@/components/ui/help-tip";
import { AgentCard } from "./agent-card"; import { AgentCard } from "./agent-card";
interface AgentGridProps { interface AgentGridProps {
title: string; title: string;
/** Optional hover explanation of what this section groups e.g. which
* roles fold into "Leadership". Falsy renders the bare heading. */
titleHint?: string;
agents: AgentDefinition[]; agents: AgentDefinition[];
agentStatuses: Record<string, AgentStatusResponse>; agentStatuses: Record<string, AgentStatusResponse>;
agentUsage?: Record<string, AgentUsageRow>; agentUsage?: Record<string, AgentUsageRow>;
@@ -20,6 +24,7 @@ const GRID_COLS = "grid-cols-[repeat(auto-fill,minmax(17rem,1fr))]";
export function AgentGrid({ export function AgentGrid({
title, title,
titleHint,
agents, agents,
agentStatuses, agentStatuses,
agentUsage, agentUsage,
@@ -28,12 +33,16 @@ export function AgentGrid({
return ( return (
<div> <div>
<div className="mb-3 flex items-center gap-2"> <div className="mb-3 flex items-center gap-2">
<h2 className="text-sm font-semibold uppercase tracking-wide text-muted-foreground"> <HelpTip label={titleHint}>
{title} <h2 className="text-sm font-semibold uppercase tracking-wide text-muted-foreground w-fit">
</h2> {title}
<Badge variant="secondary" className="text-xs"> </h2>
{agents.length} </HelpTip>
</Badge> <HelpTip label={`${agents.length} agent${agents.length === 1 ? "" : "s"} in this section`}>
<Badge variant="secondary" className="text-xs">
{agents.length}
</Badge>
</HelpTip>
</div> </div>
<div className={"grid gap-3 " + GRID_COLS}> <div className={"grid gap-3 " + GRID_COLS}>
{isLoading {isLoading
+26 -21
View File
@@ -13,6 +13,7 @@ import {
SelectValue, SelectValue,
} from "@/components/ui/select"; } from "@/components/ui/select";
import { Badge } from "@/components/ui/badge"; import { Badge } from "@/components/ui/badge";
import { HelpTip } from "@/components/ui/help-tip";
import { User, Users } from "lucide-react"; import { User, Users } from "lucide-react";
import { resolveToSlug } from "@/lib/agent-utils"; import { resolveToSlug } from "@/lib/agent-utils";
@@ -151,30 +152,34 @@ export function AgentSelector({
onValueChange={handleValueChange} onValueChange={handleValueChange}
disabled={disabled || isLoading} disabled={disabled || isLoading}
> >
<SelectTrigger className="w-full"> <HelpTip label="Agents are grouped by team below — Board, Main PM, then each cell">
<SelectValue placeholder={placeholder}> <SelectTrigger className="w-full">
{selectedAgent ? ( <SelectValue placeholder={placeholder}>
<div className="flex items-center gap-2"> {selectedAgent ? (
<User className="h-4 w-4" /> <div className="flex items-center gap-2">
<span>{selectedAgent.name}</span> <User className="h-4 w-4" />
{selectedAgent.role && ( <span>{selectedAgent.name}</span>
<Badge variant="outline" className="text-xs"> {selectedAgent.role && (
{ROLE_LABELS[selectedAgent.role] || selectedAgent.role} <Badge variant="outline" className="text-xs">
</Badge> {ROLE_LABELS[selectedAgent.role] || selectedAgent.role}
)} </Badge>
</div> )}
) : ( </div>
placeholder ) : (
)} placeholder
</SelectValue> )}
</SelectTrigger> </SelectValue>
</SelectTrigger>
</HelpTip>
<SelectContent> <SelectContent>
{allowClear && value && ( {allowClear && value && (
<SelectItem value="__clear__" className="text-muted-foreground"> <SelectItem value="__clear__" className="text-muted-foreground">
<span className="flex items-center gap-2"> <HelpTip label="Clears the current assignment">
<Users className="h-4 w-4" /> <span className="flex items-center gap-2">
Unassigned <Users className="h-4 w-4" />
</span> Unassigned
</span>
</HelpTip>
</SelectItem> </SelectItem>
)} )}
@@ -2,6 +2,7 @@ import Link from "next/link";
import { formatDistanceToNow } from "date-fns"; import { formatDistanceToNow } from "date-fns";
import { AgentStatusResponse } from "@/types"; import { AgentStatusResponse } from "@/types";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { HelpTip } from "@/components/ui/help-tip";
import { Activity, FileText, Clock, AlertCircle } from "lucide-react"; import { Activity, FileText, Clock, AlertCircle } from "lucide-react";
import { AgentStateBadge } from "./agent-state-badge"; import { AgentStateBadge } from "./agent-state-badge";
@@ -29,13 +30,15 @@ export function AgentStatusCards({ agent }: AgentStatusCardsProps) {
</CardHeader> </CardHeader>
<CardContent> <CardContent>
{agent.task_id ? ( {agent.task_id ? (
<Link <HelpTip label={`Full task id: ${agent.task_id}`}>
prefetch={false} <Link
href={"/tasks/" + agent.task_id} prefetch={false}
className="text-blue-500 hover:underline" href={"/tasks/" + agent.task_id}
> className="text-blue-500 hover:underline w-fit inline-block"
{agent.task_id.slice(0, 8)}... >
</Link> {agent.task_id.slice(0, 8)}...
</Link>
</HelpTip>
) : ( ) : (
<span className="text-muted-foreground">No task assigned</span> <span className="text-muted-foreground">No task assigned</span>
)} )}
@@ -49,7 +52,11 @@ export function AgentStatusCards({ agent }: AgentStatusCardsProps) {
</CardHeader> </CardHeader>
<CardContent> <CardContent>
{agent.started_at ? ( {agent.started_at ? (
<span>{formatDistanceToNow(new Date(agent.started_at))} ago</span> <HelpTip label={new Date(agent.started_at).toLocaleString()}>
<span className="w-fit inline-block">
{formatDistanceToNow(new Date(agent.started_at))} ago
</span>
</HelpTip>
) : ( ) : (
<span className="text-muted-foreground">Not started</span> <span className="text-muted-foreground">Not started</span>
)} )}
@@ -62,17 +69,22 @@ export function AgentStatusCards({ agent }: AgentStatusCardsProps) {
<AlertCircle className="h-4 w-4 text-muted-foreground" /> <AlertCircle className="h-4 w-4 text-muted-foreground" />
</CardHeader> </CardHeader>
<CardContent> <CardContent>
<span <HelpTip label="Errors this agent has hit since its current session started">
className={ <span
agent.error_count > 0 ? "text-red-600 font-semibold" : "" className={
} "w-fit inline-block " +
> (agent.error_count > 0 ? "text-red-600 font-semibold" : "")
{agent.error_count} }
</span> >
{agent.error_count}
</span>
</HelpTip>
{agent.waiting_for && ( {agent.waiting_for && (
<p className="text-xs text-yellow-600 mt-1 truncate"> <HelpTip label="This agent is blocked here and needs human input to continue">
Waiting: {agent.waiting_for} <p className="text-xs text-yellow-600 mt-1 truncate w-fit">
</p> Waiting: {agent.waiting_for}
</p>
</HelpTip>
)} )}
</CardContent> </CardContent>
</Card> </Card>
@@ -13,6 +13,7 @@ import {
DialogTrigger, DialogTrigger,
DialogDescription, DialogDescription,
} from "@/components/ui/dialog"; } from "@/components/ui/dialog";
import { HelpTip } from "@/components/ui/help-tip";
import { Send } from "lucide-react"; import { Send } from "lucide-react";
import { toast } from "sonner"; import { toast } from "sonner";
@@ -48,12 +49,14 @@ export function ResolveWaitDialog({ agentId }: ResolveWaitDialogProps) {
return ( return (
<Dialog open={open} onOpenChange={handleOpenChange}> <Dialog open={open} onOpenChange={handleOpenChange}>
<DialogTrigger asChild> <HelpTip label="Send the agent the information it needs to unblock and resume">
<Button> <DialogTrigger asChild>
<Send className="h-4 w-4 mr-2" /> <Button>
Resolve Wait <Send className="h-4 w-4 mr-2" />
</Button> Resolve Wait
</DialogTrigger> </Button>
</DialogTrigger>
</HelpTip>
<DialogContent> <DialogContent>
<DialogHeader> <DialogHeader>
<DialogTitle>Resolve Agent Wait</DialogTitle> <DialogTitle>Resolve Agent Wait</DialogTitle>
@@ -63,7 +66,9 @@ export function ResolveWaitDialog({ agentId }: ResolveWaitDialogProps) {
</DialogHeader> </DialogHeader>
<div className="space-y-4"> <div className="space-y-4">
<div className="space-y-2"> <div className="space-y-2">
<Label htmlFor="resolution">Resolution Message</Label> <HelpTip label="Delivered to the agent verbatim as the answer to what it's waiting on">
<Label htmlFor="resolution" className="w-fit">Resolution Message</Label>
</HelpTip>
<Textarea <Textarea
id="resolution" id="resolution"
value={resolution} value={resolution}
@@ -73,12 +78,18 @@ export function ResolveWaitDialog({ agentId }: ResolveWaitDialogProps) {
/> />
</div> </div>
<div className="flex justify-end gap-2"> <div className="flex justify-end gap-2">
<Button variant="outline" onClick={() => setOpen(false)}> <HelpTip label="Closes without sending anything to the agent">
Cancel <Button variant="outline" onClick={() => setOpen(false)}>
</Button> Cancel
<Button onClick={handleResolve} disabled={resolveWait.isPending}> </Button>
{resolveWait.isPending ? "Sending..." : "Send Resolution"} </HelpTip>
</Button> <HelpTip label="Delivers the message above to the agent so it can resume">
<span>
<Button onClick={handleResolve} disabled={resolveWait.isPending}>
{resolveWait.isPending ? "Sending..." : "Send Resolution"}
</Button>
</span>
</HelpTip>
</div> </div>
</div> </div>
</DialogContent> </DialogContent>
@@ -15,6 +15,7 @@ import {
DialogDescription, DialogDescription,
} from "@/components/ui/dialog"; } from "@/components/ui/dialog";
import { DropdownMenuItem } from "@/components/ui/dropdown-menu"; import { DropdownMenuItem } from "@/components/ui/dropdown-menu";
import { HelpTip } from "@/components/ui/help-tip";
import { Play } from "lucide-react"; import { Play } from "lucide-react";
import { toast } from "sonner"; import { toast } from "sonner";
@@ -70,10 +71,12 @@ export function SpawnAgentDialog({
}; };
const defaultTrigger = ( const defaultTrigger = (
<DropdownMenuItem onSelect={(e) => e.preventDefault()}> <HelpTip label="Start this agent's container, optionally pre-claiming a task" side="left">
<Play className="h-4 w-4 mr-2" /> <DropdownMenuItem onSelect={(e) => e.preventDefault()}>
Spawn <Play className="h-4 w-4 mr-2" />
</DropdownMenuItem> Spawn
</DropdownMenuItem>
</HelpTip>
); );
return ( return (
@@ -88,7 +91,9 @@ export function SpawnAgentDialog({
</DialogHeader> </DialogHeader>
<div className="space-y-4"> <div className="space-y-4">
<div className="space-y-2"> <div className="space-y-2">
<Label htmlFor="taskId">Task ID (optional)</Label> <HelpTip label="Pre-claims this task on spawn instead of pulling from the pool">
<Label htmlFor="taskId" className="w-fit">Task ID (optional)</Label>
</HelpTip>
<Input <Input
id="taskId" id="taskId"
value={taskId} value={taskId}
@@ -97,7 +102,9 @@ export function SpawnAgentDialog({
/> />
</div> </div>
<div className="space-y-2"> <div className="space-y-2">
<Label htmlFor="initialPrompt">Initial Prompt (optional)</Label> <HelpTip label="Extra instructions passed to the agent's first turn">
<Label htmlFor="initialPrompt" className="w-fit">Initial Prompt (optional)</Label>
</HelpTip>
<Input <Input
id="initialPrompt" id="initialPrompt"
value={initialPrompt} value={initialPrompt}
@@ -106,12 +113,18 @@ export function SpawnAgentDialog({
/> />
</div> </div>
<div className="flex justify-end gap-2"> <div className="flex justify-end gap-2">
<Button variant="outline" onClick={() => setOpen(false)}> <HelpTip label="Closes without spawning">
Cancel <Button variant="outline" onClick={() => setOpen(false)}>
</Button> Cancel
<Button onClick={handleSpawn} disabled={spawnAgent.isPending}> </Button>
{spawnAgent.isPending ? "Spawning..." : "Spawn Agent"} </HelpTip>
</Button> <HelpTip label="Already-running agents are skipped — no duplicate container is started">
<span>
<Button onClick={handleSpawn} disabled={spawnAgent.isPending}>
{spawnAgent.isPending ? "Spawning..." : "Spawn Agent"}
</Button>
</span>
</HelpTip>
</div> </div>
</div> </div>
</DialogContent> </DialogContent>
+12 -5
View File
@@ -15,6 +15,7 @@ import {
TooltipContent, TooltipContent,
TooltipTrigger, TooltipTrigger,
} from "@/components/ui/tooltip"; } from "@/components/ui/tooltip";
import { HelpTip } from "@/components/ui/help-tip";
import { useAgentStream, ConnectionState } from "@/hooks/use-websocket"; import { useAgentStream, ConnectionState } from "@/hooks/use-websocket";
import { Wifi, WifiOff, Loader2, Trash2 } from "lucide-react"; import { Wifi, WifiOff, Loader2, Trash2 } from "lucide-react";
@@ -79,9 +80,11 @@ export function AgentStreamViewer({
</CardDescription> </CardDescription>
</div> </div>
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<Badge className={stateColors[state] + " text-white"}> <HelpTip label="Live WebSocket connection carrying this agent's output — auto-reconnects on drop">
{stateLabels[state]} <Badge className={stateColors[state] + " text-white"}>
</Badge> {stateLabels[state]}
</Badge>
</HelpTip>
{streamChunks.length > 0 && ( {streamChunks.length > 0 && (
<Tooltip> <Tooltip>
<TooltipTrigger asChild> <TooltipTrigger asChild>
@@ -117,8 +120,12 @@ export function AgentStreamViewer({
)} )}
</pre> </pre>
<div className="flex justify-between items-center mt-2 text-sm text-muted-foreground"> <div className="flex justify-between items-center mt-2 text-sm text-muted-foreground">
<span>{streamChunks.length} chunks received</span> <HelpTip label="Number of stream messages received over this WebSocket connection">
<span>{streamOutput.length} characters</span> <span className="w-fit">{streamChunks.length} chunks received</span>
</HelpTip>
<HelpTip label="Total length of the accumulated output text below">
<span className="w-fit">{streamOutput.length} characters</span>
</HelpTip>
</div> </div>
</CardContent> </CardContent>
</Card> </Card>
@@ -2,6 +2,7 @@ import Link from "next/link";
import { WaitingAgent } from "@/types"; import { WaitingAgent } from "@/types";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import { HelpTip } from "@/components/ui/help-tip";
import { AlertTriangle } from "lucide-react"; import { AlertTriangle } from "lucide-react";
import { getAgentDisplayName } from "@/lib/agent-utils"; import { getAgentDisplayName } from "@/lib/agent-utils";
@@ -15,10 +16,12 @@ export function WaitingAgentsAlert({ waitingAgents }: WaitingAgentsAlertProps) {
return ( return (
<Card className="border-orange-500/50"> <Card className="border-orange-500/50">
<CardHeader> <CardHeader>
<CardTitle className="text-orange-500 flex items-center gap-2"> <HelpTip label="Agents blocked in a waiting_long state — refreshed every 10s" side="right">
<AlertTriangle className="h-5 w-5" /> <CardTitle className="text-orange-500 flex items-center gap-2 w-fit">
Agents Waiting for Input <AlertTriangle className="h-5 w-5" />
</CardTitle> Agents Waiting for Input
</CardTitle>
</HelpTip>
</CardHeader> </CardHeader>
<CardContent> <CardContent>
<div className="space-y-2"> <div className="space-y-2">
@@ -31,15 +34,19 @@ export function WaitingAgentsAlert({ waitingAgents }: WaitingAgentsAlertProps) {
<span className="font-medium"> <span className="font-medium">
{getAgentDisplayName(agent.agent_id)} {getAgentDisplayName(agent.agent_id)}
</span> </span>
<span className="text-muted-foreground ml-2"> <HelpTip label="This agent is idle and will not progress until resolved">
waiting for: {agent.waiting_for} <span className="text-muted-foreground ml-2">
</span> waiting for: {agent.waiting_for}
</span>
</HelpTip>
</div> </div>
<Button variant="outline" size="sm" asChild> <HelpTip label="Opens this agent's detail page to send the resolution it needs">
<Link href={"/agents/" + agent.agent_id} prefetch={false}> <Button variant="outline" size="sm" asChild>
Resolve <Link href={"/agents/" + agent.agent_id} prefetch={false}>
</Link> Resolve
</Button> </Link>
</Button>
</HelpTip>
</div> </div>
))} ))}
</div> </div>
@@ -12,6 +12,7 @@ import { FlaggedItemsPanel } from "./flagged-items-panel";
import { ReportsPanel } from "./reports-panel"; import { ReportsPanel } from "./reports-panel";
import { FindingsQueuePanel } from "./findings-queue-panel"; import { FindingsQueuePanel } from "./findings-queue-panel";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import { HelpTip } from "@/components/ui/help-tip";
import { FileText } from "lucide-react"; import { FileText } from "lucide-react";
import { toast } from "sonner"; import { toast } from "sonner";
import { usePageRefresh } from "@/hooks"; import { usePageRefresh } from "@/hooks";
@@ -56,21 +57,27 @@ export function AuditorDashboard() {
{/* Header */} {/* Header */}
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
<div> <div>
<h1 className="text-3xl font-bold tracking-tight"> <HelpTip label="The Auditor is a silent observer — it doesn't act on tasks, only flags and reports to the CEO">
Auditor Dashboard <h1 className="text-3xl font-bold tracking-tight">
</h1> Auditor Dashboard
</h1>
</HelpTip>
<p className="text-muted-foreground"> <p className="text-muted-foreground">
Quality oversight, flagging, and reporting Quality oversight, flagging, and reporting
</p> </p>
</div> </div>
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<Button <HelpTip label="Creates a draft audit-summary report you can edit and send from Reports below">
onClick={handleGenerateReport} <span>
disabled={createReport.isPending} <Button
> onClick={handleGenerateReport}
<FileText className="h-4 w-4 mr-2" /> disabled={createReport.isPending}
Generate Report >
</Button> <FileText className="h-4 w-4 mr-2" />
Generate Report
</Button>
</span>
</HelpTip>
</div> </div>
</div> </div>
@@ -7,6 +7,7 @@ import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input"; import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label"; import { Label } from "@/components/ui/label";
import { Textarea } from "@/components/ui/textarea"; import { Textarea } from "@/components/ui/textarea";
import { HelpTip } from "@/components/ui/help-tip";
import { import {
Dialog, Dialog,
DialogContent, DialogContent,
@@ -102,7 +103,9 @@ export function CreateFlagDialog({
</DialogHeader> </DialogHeader>
<form onSubmit={handleSubmit} className="space-y-4 mt-4"> <form onSubmit={handleSubmit} className="space-y-4 mt-4">
<div className="space-y-2"> <div className="space-y-2">
<Label htmlFor="title">Title *</Label> <HelpTip label="Required. Short summary shown atop the flag in the queue">
<Label htmlFor="title">Title *</Label>
</HelpTip>
<Input <Input
id="title" id="title"
value={title} value={title}
@@ -113,7 +116,9 @@ export function CreateFlagDialog({
</div> </div>
<div className="space-y-2"> <div className="space-y-2">
<Label htmlFor="description">Description *</Label> <HelpTip label="Required. Full explanation, shown when the flag is expanded">
<Label htmlFor="description">Description *</Label>
</HelpTip>
<Textarea <Textarea
id="description" id="description"
value={description} value={description}
@@ -126,7 +131,9 @@ export function CreateFlagDialog({
<div className="grid grid-cols-2 gap-4"> <div className="grid grid-cols-2 gap-4">
<div className="space-y-2"> <div className="space-y-2">
<Label>Severity</Label> <HelpTip label="Info/Warning are tracked only; Urgent flags surface a direct Report-to-CEO action">
<Label>Severity</Label>
</HelpTip>
<Select <Select
value={severity} value={severity}
onValueChange={(v) => setSeverity(v as FlagSeverity)} onValueChange={(v) => setSeverity(v as FlagSeverity)}
@@ -144,7 +151,9 @@ export function CreateFlagDialog({
</Select> </Select>
</div> </div>
<div className="space-y-2"> <div className="space-y-2">
<Label>Category</Label> <HelpTip label="Groups this flag for filtering and reporting purposes">
<Label>Category</Label>
</HelpTip>
<Select value={category} onValueChange={setCategory}> <Select value={category} onValueChange={setCategory}>
<SelectTrigger> <SelectTrigger>
<SelectValue /> <SelectValue />
@@ -162,7 +171,9 @@ export function CreateFlagDialog({
<div className="grid grid-cols-2 gap-4"> <div className="grid grid-cols-2 gap-4">
<div className="space-y-2"> <div className="space-y-2">
<Label htmlFor="task">Related Task ID (optional)</Label> <HelpTip label="Optional. Links this flag to a task — shown as a quick-link in the flags list">
<Label htmlFor="task">Related Task ID (optional)</Label>
</HelpTip>
<Input <Input
id="task" id="task"
value={relatedTaskId} value={relatedTaskId}
@@ -171,7 +182,9 @@ export function CreateFlagDialog({
/> />
</div> </div>
<div className="space-y-2"> <div className="space-y-2">
<Label htmlFor="agent">Related Agent ID (optional)</Label> <HelpTip label="Optional. Associates this flag with a specific agent for audit tracking">
<Label htmlFor="agent">Related Agent ID (optional)</Label>
</HelpTip>
<Input <Input
id="agent" id="agent"
value={relatedAgentId} value={relatedAgentId}
@@ -182,19 +195,25 @@ export function CreateFlagDialog({
</div> </div>
<div className="flex justify-end gap-2 pt-4"> <div className="flex justify-end gap-2 pt-4">
<Button <HelpTip label="Discards this draft and closes the dialog">
type="button" <Button
variant="outline" type="button"
onClick={() => { variant="outline"
onOpenChange(false); onClick={() => {
resetForm(); onOpenChange(false);
}} resetForm();
> }}
Cancel >
</Button> Cancel
<Button type="submit" disabled={createFlag.isPending}> </Button>
{createFlag.isPending ? "Creating..." : "Create Flag"} </HelpTip>
</Button> <HelpTip label="Saves the flag; it appears in Flagged Items right away">
<span>
<Button type="submit" disabled={createFlag.isPending}>
{createFlag.isPending ? "Creating..." : "Create Flag"}
</Button>
</span>
</HelpTip>
</div> </div>
</form> </form>
</DialogContent> </DialogContent>
@@ -46,12 +46,16 @@ export function FindingsQueuePanel({
<CardHeader className="pb-3"> <CardHeader className="pb-3">
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<CardTitle className="text-lg flex items-center gap-2"> <HelpTip label="Unresolved structured findings from QA, PR-gate, PM, and CEO review bounces">
<ListChecks className="h-5 w-5" /> <CardTitle className="text-lg flex items-center gap-2">
Open Findings <ListChecks className="h-5 w-5" />
</CardTitle> Open Findings
</CardTitle>
</HelpTip>
{sorted.length > 0 && ( {sorted.length > 0 && (
<Badge variant="destructive">{sorted.length}</Badge> <HelpTip label="Number of currently open findings in this list">
<Badge variant="destructive">{sorted.length}</Badge>
</HelpTip>
)} )}
</div> </div>
</div> </div>
@@ -64,10 +68,12 @@ export function FindingsQueuePanel({
))} ))}
</div> </div>
) : sorted.length === 0 ? ( ) : sorted.length === 0 ? (
<div className="text-center py-8 text-muted-foreground text-sm"> <HelpTip label="No blocker/major/minor/nit findings are currently open on any task">
<ListChecks className="h-8 w-8 mx-auto mb-2 opacity-50" /> <div className="text-center py-8 text-muted-foreground text-sm">
No open review findings <ListChecks className="h-8 w-8 mx-auto mb-2 opacity-50" />
</div> No open review findings
</div>
</HelpTip>
) : ( ) : (
<ScrollArea className="h-[400px] pr-4"> <ScrollArea className="h-[400px] pr-4">
<div className="space-y-3"> <div className="space-y-3">
@@ -77,13 +83,15 @@ export function FindingsQueuePanel({
className="p-4 rounded-lg border bg-muted/50" className="p-4 rounded-lg border bg-muted/50"
> >
<div className="flex items-center gap-2 mb-1 flex-wrap"> <div className="flex items-center gap-2 mb-1 flex-wrap">
<Badge <HelpTip label="Severity: blocker > major > minor > nit, sorted most severe first">
className={ <Badge
(severityColors[finding.severity] ?? "") + " text-xs" className={
} (severityColors[finding.severity] ?? "") + " text-xs"
> }
{finding.severity} >
</Badge> {finding.severity}
</Badge>
</HelpTip>
<HelpTip label="Where this finding was raised — QA review, PR gate, PM review, or CEO approval"> <HelpTip label="Where this finding was raised — QA review, PR gate, PM review, or CEO approval">
<Badge variant="outline" className="text-xs"> <Badge variant="outline" className="text-xs">
{finding.origin} {finding.origin}
@@ -95,15 +103,19 @@ export function FindingsQueuePanel({
</span> </span>
</HelpTip> </HelpTip>
</div> </div>
<p className="text-sm text-muted-foreground mb-2"> <HelpTip label="Shows actual → expected → criterion text, whichever is available first">
{finding.actual ?? finding.expected ?? finding.criterion ?? "—"} <p className="text-sm text-muted-foreground mb-2">
</p> {finding.actual ?? finding.expected ?? finding.criterion ?? "—"}
</p>
</HelpTip>
<div className="flex items-center gap-4 text-xs text-muted-foreground"> <div className="flex items-center gap-4 text-xs text-muted-foreground">
{finding.file && ( {finding.file && (
<span className="font-mono"> <HelpTip label="Repo-relative file and line where this issue was found">
{finding.file} <span className="font-mono">
{finding.line ? `:${finding.line}` : ""} {finding.file}
</span> {finding.line ? `:${finding.line}` : ""}
</span>
</HelpTip>
)} )}
<Link <Link
href={"/tasks/" + finding.task_id} href={"/tasks/" + finding.task_id}
+41 -27
View File
@@ -52,30 +52,40 @@ export function FlaggedItem({
> >
<div className="flex items-start justify-between gap-4"> <div className="flex items-start justify-between gap-4">
<div className="flex items-start gap-3 flex-1 min-w-0"> <div className="flex items-start gap-3 flex-1 min-w-0">
<span className="text-xl">{severityEmoji[flag.severity]}</span> <HelpTip label="Green = info, yellow = warning, red = urgent severity">
<span className="text-xl">{severityEmoji[flag.severity]}</span>
</HelpTip>
<div className="flex-1 min-w-0"> <div className="flex-1 min-w-0">
<div className="flex items-center gap-2 mb-1 flex-wrap"> <div className="flex items-center gap-2 mb-1 flex-wrap">
<span className="font-medium text-sm">{flag.title}</span> <span className="font-medium text-sm">{flag.title}</span>
<Badge className={severityColors[flag.severity] + " text-xs"}> <HelpTip label="Determines this flag's priority — Urgent sorts first in the queue">
{flag.severity} <Badge className={severityColors[flag.severity] + " text-xs"}>
</Badge> {flag.severity}
<Badge variant="outline" className="text-xs">
{flag.category}
</Badge>
{isResolved && (
<Badge className="bg-green-100 text-green-700 text-xs">
Resolved
</Badge> </Badge>
</HelpTip>
<HelpTip label="Category assigned when this flag was created (quality, process, security, etc.)">
<Badge variant="outline" className="text-xs">
{flag.category}
</Badge>
</HelpTip>
{isResolved && (
<HelpTip label="Hidden from the default Unresolved filter (still visible under Resolved or All)">
<Badge className="bg-green-100 text-green-700 text-xs">
Resolved
</Badge>
</HelpTip>
)} )}
</div> </div>
<p className="text-sm text-muted-foreground mb-2"> <p className="text-sm text-muted-foreground mb-2">
{flag.description} {flag.description}
</p> </p>
<div className="flex items-center gap-4 text-xs text-muted-foreground"> <div className="flex items-center gap-4 text-xs text-muted-foreground">
<span className="flex items-center gap-1"> <HelpTip label={new Date(flag.created_at).toLocaleString()}>
<Clock className="h-3 w-3" /> <span className="flex items-center gap-1">
{formatTime(flag.created_at)} <Clock className="h-3 w-3" />
</span> {formatTime(flag.created_at)}
</span>
</HelpTip>
{flag.related_task_id && ( {flag.related_task_id && (
<Link href={"/tasks/" + flag.related_task_id} prefetch={false}> <Link href={"/tasks/" + flag.related_task_id} prefetch={false}>
<HelpTip label="Short task ID — first 8 characters of the full task identifier"> <HelpTip label="Short task ID — first 8 characters of the full task identifier">
@@ -99,23 +109,27 @@ export function FlaggedItem({
</HelpTip> </HelpTip>
</Link> </Link>
)} )}
<Button <HelpTip label="Marks this flag resolved; it moves out of the default Unresolved filter">
variant="outline"
size="sm"
onClick={() => onResolve?.(flag.id)}
>
<CheckCircle className="h-4 w-4 mr-1" />
Resolve
</Button>
{flag.severity === FlagSeverity.URGENT && (
<Button <Button
variant="default" variant="outline"
size="sm" size="sm"
onClick={() => onReportToCeo?.(flag)} onClick={() => onResolve?.(flag.id)}
> >
<Send className="h-4 w-4 mr-1" /> <CheckCircle className="h-4 w-4 mr-1" />
Report CEO Resolve
</Button> </Button>
</HelpTip>
{flag.severity === FlagSeverity.URGENT && (
<HelpTip label="Only shown for Urgent-severity flags">
<Button
variant="default"
size="sm"
onClick={() => onReportToCeo?.(flag)}
>
<Send className="h-4 w-4 mr-1" />
Report CEO
</Button>
</HelpTip>
)} )}
</div> </div>
)} )}
@@ -8,6 +8,7 @@ import { Button } from "@/components/ui/button";
import { Badge } from "@/components/ui/badge"; import { Badge } from "@/components/ui/badge";
import { Skeleton } from "@/components/ui/skeleton"; import { Skeleton } from "@/components/ui/skeleton";
import { ScrollArea } from "@/components/ui/scroll-area"; import { ScrollArea } from "@/components/ui/scroll-area";
import { HelpTip } from "@/components/ui/help-tip";
import { import {
Select, Select,
SelectContent, SelectContent,
@@ -70,12 +71,16 @@ export function FlaggedItemsPanel({
<CardHeader className="pb-3"> <CardHeader className="pb-3">
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<CardTitle className="text-lg flex items-center gap-2"> <HelpTip label="Issues manually flagged by the Auditor for quality tracking">
<Flag className="h-5 w-5" /> <CardTitle className="text-lg flex items-center gap-2">
Flagged Items <Flag className="h-5 w-5" />
</CardTitle> Flagged Items
</CardTitle>
</HelpTip>
{unresolvedCount > 0 && ( {unresolvedCount > 0 && (
<Badge variant="destructive">{unresolvedCount}</Badge> <HelpTip label="Count of currently unresolved flags">
<Badge variant="destructive">{unresolvedCount}</Badge>
</HelpTip>
)} )}
</div> </div>
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
@@ -85,19 +90,23 @@ export function FlaggedItemsPanel({
setFilter(v as "all" | "unresolved" | "resolved") setFilter(v as "all" | "unresolved" | "resolved")
} }
> >
<SelectTrigger className="w-auto min-w-24 h-8"> <HelpTip label="Filter the list by resolution status">
<SelectValue /> <SelectTrigger className="w-auto min-w-24 h-8">
</SelectTrigger> <SelectValue />
</SelectTrigger>
</HelpTip>
<SelectContent> <SelectContent>
<SelectItem value="unresolved">Unresolved</SelectItem> <SelectItem value="unresolved">Unresolved</SelectItem>
<SelectItem value="resolved">Resolved</SelectItem> <SelectItem value="resolved">Resolved</SelectItem>
<SelectItem value="all">All</SelectItem> <SelectItem value="all">All</SelectItem>
</SelectContent> </SelectContent>
</Select> </Select>
<Button size="sm" onClick={() => setCreateDialogOpen(true)}> <HelpTip label="Opens a form to manually create a new quality flag">
<Plus className="h-4 w-4 mr-1" /> <Button size="sm" onClick={() => setCreateDialogOpen(true)}>
Flag <Plus className="h-4 w-4 mr-1" />
</Button> Flag
</Button>
</HelpTip>
</div> </div>
</div> </div>
</CardHeader> </CardHeader>
@@ -109,10 +118,12 @@ export function FlaggedItemsPanel({
))} ))}
</div> </div>
) : sortedFlags.length === 0 ? ( ) : sortedFlags.length === 0 ? (
<div className="text-center py-8 text-muted-foreground text-sm"> <HelpTip label="No flags match the selected status filter">
<Flag className="h-8 w-8 mx-auto mb-2 opacity-50" /> <div className="text-center py-8 text-muted-foreground text-sm">
No {filter === "all" ? "" : filter} flags <Flag className="h-8 w-8 mx-auto mb-2 opacity-50" />
</div> No {filter === "all" ? "" : filter} flags
</div>
</HelpTip>
) : ( ) : (
<ScrollArea className="h-[400px] pr-4"> <ScrollArea className="h-[400px] pr-4">
<div className="space-y-3"> <div className="space-y-3">
@@ -81,10 +81,12 @@ export function QualityMetricsPanel({
return ( return (
<Card> <Card>
<CardHeader className="pb-3"> <CardHeader className="pb-3">
<CardTitle className="text-lg flex items-center gap-2"> <HelpTip label="Rolling delivery metrics feeding the Auditor's oversight view, refreshed every 30s">
<BarChart3 className="h-5 w-5" /> <CardTitle className="text-lg flex items-center gap-2">
Quality Metrics <BarChart3 className="h-5 w-5" />
</CardTitle> Quality Metrics
</CardTitle>
</HelpTip>
</CardHeader> </CardHeader>
<CardContent> <CardContent>
{isLoading ? ( {isLoading ? (
+55 -33
View File
@@ -69,18 +69,24 @@ export function ReportsPanel({
<Card> <Card>
<CardHeader className="pb-3"> <CardHeader className="pb-3">
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
<CardTitle className="text-lg flex items-center gap-2"> <HelpTip label="Audit reports for the CEO; drafts stay editable until sent">
<FileText className="h-5 w-5" /> <CardTitle className="text-lg flex items-center gap-2">
Reports <FileText className="h-5 w-5" />
</CardTitle> Reports
<Button </CardTitle>
size="sm" </HelpTip>
onClick={handleNewReport} <HelpTip label="Creates a new draft report you can edit and send below">
disabled={createReport.isPending} <span>
> <Button
<Plus className="h-4 w-4 mr-1" /> size="sm"
New Report onClick={handleNewReport}
</Button> disabled={createReport.isPending}
>
<Plus className="h-4 w-4 mr-1" />
New Report
</Button>
</span>
</HelpTip>
</div> </div>
</CardHeader> </CardHeader>
<CardContent> <CardContent>
@@ -91,10 +97,12 @@ export function ReportsPanel({
))} ))}
</div> </div>
) : !reports || reports.length === 0 ? ( ) : !reports || reports.length === 0 ? (
<div className="text-center py-8 text-muted-foreground text-sm"> <HelpTip label="No audit reports have been generated yet — use New Report to create one">
<FileText className="h-8 w-8 mx-auto mb-2 opacity-50" /> <div className="text-center py-8 text-muted-foreground text-sm">
No reports yet <FileText className="h-8 w-8 mx-auto mb-2 opacity-50" />
</div> No reports yet
</div>
</HelpTip>
) : ( ) : (
<ScrollArea className="h-[300px] pr-4"> <ScrollArea className="h-[300px] pr-4">
<div className="space-y-3"> <div className="space-y-3">
@@ -107,19 +115,29 @@ export function ReportsPanel({
> >
<div className="flex-1 min-w-0"> <div className="flex-1 min-w-0">
<div className="flex items-center gap-2 mb-1"> <div className="flex items-center gap-2 mb-1">
<Badge variant={isDraft ? "secondary" : "default"}> <HelpTip label="Draft reports aren't visible to the CEO until sent">
{isDraft ? "Draft" : "Sent"} <Badge variant={isDraft ? "secondary" : "default"}>
</Badge> {isDraft ? "Draft" : "Sent"}
</Badge>
</HelpTip>
<span className="font-medium text-sm truncate"> <span className="font-medium text-sm truncate">
{report.title} {report.title}
</span> </span>
</div> </div>
<div className="flex items-center gap-4 text-xs text-muted-foreground"> <div className="flex items-center gap-4 text-xs text-muted-foreground">
<span className="capitalize">{report.report_type}</span> <HelpTip label="Report category set when this report was created">
<span className="flex items-center gap-1"> <span className="capitalize">
<Clock className="h-3 w-3" /> {report.report_type}
{formatDate(report.created_at)} </span>
</span> </HelpTip>
<HelpTip
label={new Date(report.created_at).toLocaleString()}
>
<span className="flex items-center gap-1">
<Clock className="h-3 w-3" />
{formatDate(report.created_at)}
</span>
</HelpTip>
</div> </div>
</div> </div>
<div className="flex items-center gap-2 shrink-0"> <div className="flex items-center gap-2 shrink-0">
@@ -129,15 +147,19 @@ export function ReportsPanel({
</Button> </Button>
</HelpTip> </HelpTip>
{isDraft && ( {isDraft && (
<Button <HelpTip label="Sends this draft report to the CEO">
variant="outline" <span>
size="sm" <Button
onClick={() => handleSend(report.id)} variant="outline"
disabled={sendReport.isPending} size="sm"
> onClick={() => handleSend(report.id)}
<Send className="h-4 w-4 mr-1" /> disabled={sendReport.isPending}
Send >
</Button> <Send className="h-4 w-4 mr-1" />
Send
</Button>
</span>
</HelpTip>
)} )}
</div> </div>
</div> </div>
@@ -44,12 +44,16 @@ export function ActiveBlockersPanel({
<Card> <Card>
<CardHeader className="pb-3"> <CardHeader className="pb-3">
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
<CardTitle className="text-lg flex items-center gap-2"> <HelpTip label="Tasks in BLOCKED status, oldest first">
<AlertTriangle className="h-5 w-5 text-red-500" /> <CardTitle className="text-lg flex items-center gap-2">
Active Blockers <AlertTriangle className="h-5 w-5 text-red-500" />
</CardTitle> Active Blockers
</CardTitle>
</HelpTip>
{blockedTasks.length > 0 && ( {blockedTasks.length > 0 && (
<Badge variant="destructive">{blockedTasks.length}</Badge> <HelpTip label="Shown below — capped at 5, there may be more">
<Badge variant="destructive">{blockedTasks.length}</Badge>
</HelpTip>
)} )}
</div> </div>
</CardHeader> </CardHeader>
@@ -77,16 +81,20 @@ export function ActiveBlockersPanel({
Task #{task.id.slice(0, 8)} Task #{task.id.slice(0, 8)}
</span> </span>
</HelpTip> </HelpTip>
<Badge variant="outline" className="text-xs capitalize"> <HelpTip label="Team currently responsible for this task">
{task.team.replace(/_/g, " ")} <Badge variant="outline" className="text-xs capitalize">
</Badge> {task.team.replace(/_/g, " ")}
</Badge>
</HelpTip>
</div> </div>
<p className="text-sm truncate">{task.title}</p> <p className="text-sm truncate">{task.title}</p>
<div className="flex items-center gap-1 mt-1 text-xs text-muted-foreground"> <HelpTip label="Time since this task's last status update (or creation, if never updated)">
<Clock className="h-3 w-3" /> <div className="flex items-center gap-1 mt-1 text-xs text-muted-foreground">
Blocked for{" "} <Clock className="h-3 w-3" />
{formatDuration(task.updated_at ?? task.created_at)} Blocked for{" "}
</div> {formatDuration(task.updated_at ?? task.created_at)}
</div>
</HelpTip>
</div> </div>
</div> </div>
</Link> </Link>
@@ -95,10 +103,12 @@ export function ActiveBlockersPanel({
)} )}
<div className="mt-4 pt-3 border-t"> <div className="mt-4 pt-3 border-t">
<Link href="/tasks?status=blocked" prefetch={false}> <Link href="/tasks?status=blocked" prefetch={false}>
<Button variant="ghost" size="sm" className="w-full"> <HelpTip label="The complete blocked-task list — not capped to 5">
View All Blocked <Button variant="ghost" size="sm" className="w-full">
<ArrowRight className="h-4 w-4 ml-2" /> View All Blocked
</Button> <ArrowRight className="h-4 w-4 ml-2" />
</Button>
</HelpTip>
</Link> </Link>
</div> </div>
</CardContent> </CardContent>
@@ -9,6 +9,7 @@ import {
Clock, Clock,
} from "lucide-react"; } from "lucide-react";
import { getAgentDisplayName } from "@/lib/agent-utils"; import { getAgentDisplayName } from "@/lib/agent-utils";
import { HelpTip } from "@/components/ui/help-tip";
export interface Activity { export interface Activity {
id: string; id: string;
@@ -66,9 +67,11 @@ export function ActivityItem({ activity }: ActivityItemProps) {
<div className="mt-0.5">{icon}</div> <div className="mt-0.5">{icon}</div>
<div className="flex-1 min-w-0"> <div className="flex-1 min-w-0">
<p className="text-sm"> <p className="text-sm">
<span className="font-medium"> <HelpTip label={`Agent slug: ${activity.agent_id}`}>
{getAgentDisplayName(activity.agent_id)} <span className="font-medium">
</span>{" "} {getAgentDisplayName(activity.agent_id)}
</span>
</HelpTip>{" "}
<span className="text-muted-foreground">{label}</span> <span className="text-muted-foreground">{label}</span>
{activity.task_title && ( {activity.task_title && (
<> <>
@@ -77,9 +80,17 @@ export function ActivityItem({ activity }: ActivityItemProps) {
</> </>
)} )}
</p> </p>
<span className="text-xs text-muted-foreground"> <HelpTip
{activity.timestamp ? formatTime(activity.timestamp) : "N/A"} label={
</span> activity.timestamp
? `Exact time: ${new Date(activity.timestamp).toLocaleString()}`
: ""
}
>
<span className="text-xs text-muted-foreground">
{activity.timestamp ? formatTime(activity.timestamp) : "N/A"}
</span>
</HelpTip>
</div> </div>
</div> </div>
); );
@@ -7,6 +7,7 @@ import { Button } from "@/components/ui/button";
import { Skeleton } from "@/components/ui/skeleton"; import { Skeleton } from "@/components/ui/skeleton";
import { Shield, ArrowRight } from "lucide-react"; import { Shield, ArrowRight } from "lucide-react";
import Link from "next/link"; import Link from "next/link";
import { HelpTip } from "@/components/ui/help-tip";
interface AuditorAlertsPanelProps { interface AuditorAlertsPanelProps {
alerts: AuditorFlag[] | undefined; alerts: AuditorFlag[] | undefined;
@@ -49,12 +50,16 @@ export function AuditorAlertsPanel({
<Card> <Card>
<CardHeader className="pb-3"> <CardHeader className="pb-3">
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
<CardTitle className="text-lg flex items-center gap-2"> <HelpTip label="Unresolved flags raised by the silent Auditor role, most severe first">
<Shield className="h-5 w-5" /> <CardTitle className="text-lg flex items-center gap-2">
Auditor Alerts <Shield className="h-5 w-5" />
</CardTitle> Auditor Alerts
</CardTitle>
</HelpTip>
{unresolvedAlerts.length > 0 && ( {unresolvedAlerts.length > 0 && (
<Badge variant="destructive">{unresolvedAlerts.length}</Badge> <HelpTip label="Shown below — capped at 5, there may be more">
<Badge variant="destructive">{unresolvedAlerts.length}</Badge>
</HelpTip>
)} )}
</div> </div>
</CardHeader> </CardHeader>
@@ -77,17 +82,33 @@ export function AuditorAlertsPanel({
key={alert.id} key={alert.id}
className="flex items-start gap-3 p-3 rounded-lg border bg-muted/30" className="flex items-start gap-3 p-3 rounded-lg border bg-muted/30"
> >
<span className="text-lg">{severityEmoji[alert.severity]}</span> <HelpTip
label={`Severity: ${alert.severity}${alert.category ? `${alert.category}` : ""}`}
>
<span className="text-lg">
{severityEmoji[alert.severity]}
</span>
</HelpTip>
<div className="flex-1 min-w-0"> <div className="flex-1 min-w-0">
<div className="flex items-center gap-2 mb-1"> <div className="flex items-center gap-2 mb-1">
<span className="font-medium text-sm truncate"> <span className="font-medium text-sm truncate">
{alert.title} {alert.title}
</span> </span>
<Badge <HelpTip
className={severityColors[alert.severity] + " text-xs"} label={
alert.severity === "urgent"
? "Urgent — needs attention now"
: alert.severity === "warning"
? "Warning — worth a look soon"
: "Info — no action required"
}
> >
{alert.severity} <Badge
</Badge> className={severityColors[alert.severity] + " text-xs"}
>
{alert.severity}
</Badge>
</HelpTip>
</div> </div>
<p className="text-xs text-muted-foreground line-clamp-1"> <p className="text-xs text-muted-foreground line-clamp-1">
{alert.description} {alert.description}
@@ -99,10 +120,12 @@ export function AuditorAlertsPanel({
)} )}
<div className="mt-4 pt-3 border-t"> <div className="mt-4 pt-3 border-t">
<Link href="/auditor" prefetch={false}> <Link href="/auditor" prefetch={false}>
<Button variant="ghost" size="sm" className="w-full"> <HelpTip label="Full Auditor report, including resolved flags">
View All Flags <Button variant="ghost" size="sm" className="w-full">
<ArrowRight className="h-4 w-4 ml-2" /> View All Flags
</Button> <ArrowRight className="h-4 w-4 ml-2" />
</Button>
</HelpTip>
</Link> </Link>
</div> </div>
</CardContent> </CardContent>
@@ -225,7 +225,9 @@ export function CeoApprovalQueue({ className }: CeoApprovalQueueProps) {
<div className="min-w-0 flex-1"> <div className="min-w-0 flex-1">
<div className="flex items-center gap-2 mb-1"> <div className="flex items-center gap-2 mb-1">
{getPriorityBadge(task.priority)} {getPriorityBadge(task.priority)}
<Badge variant="outline">{task.team}</Badge> <HelpTip label="Team this task belongs to">
<Badge variant="outline">{task.team}</Badge>
</HelpTip>
</div> </div>
<Link <Link
prefetch={false} prefetch={false}
@@ -250,33 +252,39 @@ export function CeoApprovalQueue({ className }: CeoApprovalQueueProps) {
</Button> </Button>
</HelpTip> </HelpTip>
</Link> </Link>
<Button <HelpTip label="Sends this task back to needs_revision with your reason">
variant="outline" <Button
size="sm" variant="outline"
className="text-destructive hover:text-destructive" size="sm"
onClick={() => openDialog(task, "reject")} className="text-destructive hover:text-destructive"
> onClick={() => openDialog(task, "reject")}
<XCircle className="h-4 w-4 mr-1" /> >
Reject <XCircle className="h-4 w-4 mr-1" />
</Button> Reject
</Button>
</HelpTip>
{kind === "start" ? ( {kind === "start" ? (
<Button <HelpTip label="Hands the task to the Main PM to delegate to the cells">
size="sm" <Button
className="bg-blue-600 hover:bg-blue-700" size="sm"
onClick={() => openDialog(task, "start")} className="bg-blue-600 hover:bg-blue-700"
> onClick={() => openDialog(task, "start")}
<Rocket className="h-4 w-4 mr-1" /> >
Approve &amp; Start <Rocket className="h-4 w-4 mr-1" />
</Button> Approve &amp; Start
</Button>
</HelpTip>
) : ( ) : (
<Button <HelpTip label="Merges the PR and marks the task completed">
size="sm" <Button
className="bg-green-600 hover:bg-green-700" size="sm"
onClick={() => openDialog(task, "approve")} className="bg-green-600 hover:bg-green-700"
> onClick={() => openDialog(task, "approve")}
<CheckCircle2 className="h-4 w-4 mr-1" /> >
Approve <CheckCircle2 className="h-4 w-4 mr-1" />
</Button> Approve
</Button>
</HelpTip>
)} )}
</div> </div>
</div> </div>
@@ -290,9 +298,11 @@ export function CeoApprovalQueue({ className }: CeoApprovalQueueProps) {
<Clock className="h-5 w-5" /> <Clock className="h-5 w-5" />
CEO Approval Queue CEO Approval Queue
{totalCount > 0 && ( {totalCount > 0 && (
<Badge variant="secondary" className="ml-2"> <HelpTip label="Ready-to-start plus final-approval tasks combined">
{totalCount} <Badge variant="secondary" className="ml-2">
</Badge> {totalCount}
</Badge>
</HelpTip>
)} )}
</CardTitle> </CardTitle>
<CardDescription>Tasks waiting on your decision</CardDescription> <CardDescription>Tasks waiting on your decision</CardDescription>
@@ -307,17 +317,21 @@ export function CeoApprovalQueue({ className }: CeoApprovalQueueProps) {
<div className="space-y-5"> <div className="space-y-5">
{readyToStart.length > 0 && ( {readyToStart.length > 0 && (
<div className="space-y-3"> <div className="space-y-3">
<p className="text-xs font-semibold uppercase tracking-wide text-muted-foreground"> <HelpTip label="Board finished reviewing; still pending until you approve & start">
Ready to start · board reviewed <p className="text-xs font-semibold uppercase tracking-wide text-muted-foreground">
</p> Ready to start · board reviewed
</p>
</HelpTip>
{readyToStart.map((task) => renderRow(task, "start"))} {readyToStart.map((task) => renderRow(task, "start"))}
</div> </div>
)} )}
{pendingTasks.length > 0 && ( {pendingTasks.length > 0 && (
<div className="space-y-3"> <div className="space-y-3">
<p className="text-xs font-semibold uppercase tracking-wide text-muted-foreground"> <HelpTip label="Delivery work is done; this is the last gate before merge">
Final approval · work complete <p className="text-xs font-semibold uppercase tracking-wide text-muted-foreground">
</p> Final approval · work complete
</p>
</HelpTip>
{pendingTasks.map((task) => renderRow(task, "approve"))} {pendingTasks.map((task) => renderRow(task, "approve"))}
</div> </div>
)} )}
@@ -33,6 +33,7 @@ import {
} from "@/components/ui/tooltip"; } from "@/components/ui/tooltip";
import { Settings, AlertCircle } from "lucide-react"; import { Settings, AlertCircle } from "lucide-react";
import Link from "next/link"; import Link from "next/link";
import { HelpTip } from "@/components/ui/help-tip";
const SETTINGS_LABEL = "Open settings"; const SETTINGS_LABEL = "Open settings";
@@ -137,7 +138,11 @@ export function CommandCenter() {
{/* Section 1: Quick Actions + the four key cards */} {/* Section 1: Quick Actions + the four key cards */}
<section> <section>
<h2 className="text-lg font-semibold mb-4">Quick Actions</h2> <HelpTip label="One-click shortcuts to the pages you visit most often">
<h2 className="text-lg font-semibold mb-4 inline-block">
Quick Actions
</h2>
</HelpTip>
<QuickActionsBar /> <QuickActionsBar />
</section> </section>
@@ -153,7 +158,11 @@ export function CommandCenter() {
{/* Section 2: Team Health (team cards + Task Intake + Secretary) */} {/* Section 2: Team Health (team cards + Task Intake + Secretary) */}
<section> <section>
<h2 className="text-lg font-semibold mb-4">Team Health</h2> <HelpTip label="Per-team blocked ratio and throughput, plus the on-demand agents">
<h2 className="text-lg font-semibold mb-4 inline-block">
Team Health
</h2>
</HelpTip>
<TeamHealthCards <TeamHealthCards
teams={overview?.health_status} teams={overview?.health_status}
isLoading={loadingOverview} isLoading={loadingOverview}
@@ -61,7 +61,9 @@ export function KeyMetricsPanel({ metrics, isLoading }: KeyMetricsProps) {
return ( return (
<Card> <Card>
<CardHeader className="pb-3"> <CardHeader className="pb-3">
<CardTitle className="text-lg">Key Metrics</CardTitle> <HelpTip label="Org-wide 7-day delivery snapshot from DashboardService">
<CardTitle className="text-lg">Key Metrics</CardTitle>
</HelpTip>
</CardHeader> </CardHeader>
<CardContent> <CardContent>
{isLoading ? ( {isLoading ? (
@@ -25,6 +25,7 @@ import { Textarea } from "@/components/ui/textarea";
import { Label } from "@/components/ui/label"; import { Label } from "@/components/ui/label";
import { BookOpen, CheckCircle2, XCircle } from "lucide-react"; import { BookOpen, CheckCircle2, XCircle } from "lucide-react";
import { toast } from "sonner"; import { toast } from "sonner";
import { HelpTip } from "@/components/ui/help-tip";
const _MIN_REASON = 4; const _MIN_REASON = 4;
@@ -89,9 +90,15 @@ export function PlaybookReviewQueue({ className }: { className?: string }) {
<Card className={className}> <Card className={className}>
<CardHeader> <CardHeader>
<CardTitle className="flex items-center gap-2"> <CardTitle className="flex items-center gap-2">
<BookOpen className="h-5 w-5" /> <HelpTip label="Procedures agents drafted from real task runs, awaiting your curation call">
Playbook Review <span className="inline-flex items-center gap-2">
<Badge variant="secondary">{drafts.length}</Badge> <BookOpen className="h-5 w-5" />
Playbook Review
</span>
</HelpTip>
<HelpTip label="Drafts awaiting a decision">
<Badge variant="secondary">{drafts.length}</Badge>
</HelpTip>
</CardTitle> </CardTitle>
<CardDescription> <CardDescription>
Drafted playbooks awaiting your approval approved ones are indexed Drafted playbooks awaiting your approval approved ones are indexed
@@ -106,11 +113,17 @@ export function PlaybookReviewQueue({ className }: { className?: string }) {
> >
<div className="mb-1 flex items-center gap-2"> <div className="mb-1 flex items-center gap-2">
<span className="font-medium">{pb.title}</span> <span className="font-medium">{pb.title}</span>
{pb.team && <Badge variant="outline">{pb.team}</Badge>} {pb.team && (
<HelpTip label="Team that authored this playbook">
<Badge variant="outline">{pb.team}</Badge>
</HelpTip>
)}
{pb.tags.map((t) => ( {pb.tags.map((t) => (
<Badge key={t} variant="secondary" className="text-xs"> <HelpTip key={t} label="Search/filter tag for this playbook">
{t} <Badge variant="secondary" className="text-xs">
</Badge> {t}
</Badge>
</HelpTip>
))} ))}
</div> </div>
<p className="text-sm text-muted-foreground"> <p className="text-sm text-muted-foreground">
@@ -120,27 +133,31 @@ export function PlaybookReviewQueue({ className }: { className?: string }) {
{pb.procedure} {pb.procedure}
</pre> </pre>
<div className="mt-3 flex flex-col-reverse gap-2 sm:flex-row sm:items-center sm:justify-end"> <div className="mt-3 flex flex-col-reverse gap-2 sm:flex-row sm:items-center sm:justify-end">
<Button <HelpTip label="Archives this draft — it is never indexed">
variant="outline" <Button
size="sm" variant="outline"
className="text-destructive hover:text-destructive" size="sm"
onClick={() => setRejecting(pb)} className="text-destructive hover:text-destructive"
> onClick={() => setRejecting(pb)}
<XCircle className="mr-1 h-4 w-4" /> >
Reject <XCircle className="mr-1 h-4 w-4" />
</Button> Reject
<Button </Button>
size="sm" </HelpTip>
className="bg-green-600 hover:bg-green-700" <HelpTip label="Indexes this playbook so it's auto-suggested to agents">
disabled={ <Button
approveMutation.isPending && size="sm"
approveMutation.variables === pb.id className="bg-green-600 hover:bg-green-700"
} disabled={
onClick={() => approveMutation.mutate(pb.id)} approveMutation.isPending &&
> approveMutation.variables === pb.id
<CheckCircle2 className="mr-1 h-4 w-4" /> }
Approve onClick={() => approveMutation.mutate(pb.id)}
</Button> >
<CheckCircle2 className="mr-1 h-4 w-4" />
Approve
</Button>
</HelpTip>
</div> </div>
</div> </div>
))} ))}
@@ -27,6 +27,7 @@ import {
TooltipProvider, TooltipProvider,
TooltipTrigger, TooltipTrigger,
} from "@/components/ui/tooltip"; } from "@/components/ui/tooltip";
import { HelpTip } from "@/components/ui/help-tip";
import { import {
GitPullRequest, GitPullRequest,
ExternalLink, ExternalLink,
@@ -139,9 +140,11 @@ export function PrReviewQueue({ className }: PrReviewQueueProps) {
<CardTitle className="flex items-center gap-2"> <CardTitle className="flex items-center gap-2">
<GitPullRequest className="h-5 w-5" /> <GitPullRequest className="h-5 w-5" />
PR Reviews PR Reviews
<Badge variant="secondary" className="ml-2"> <HelpTip label="External PRs in review or awaiting your call">
{items.length} <Badge variant="secondary" className="ml-2">
</Badge> {items.length}
</Badge>
</HelpTip>
</CardTitle> </CardTitle>
<CardDescription> <CardDescription>
External PRs the org is reviewing or has reviewed the reviewer External PRs the org is reviewing or has reviewed the reviewer
@@ -175,14 +178,18 @@ export function PrReviewQueue({ className }: PrReviewQueueProps) {
{task.title} {task.title}
</Link> </Link>
{awaiting ? ( {awaiting ? (
<Badge variant="secondary" className="shrink-0"> <HelpTip label="The reviewer finished — pick Supersede or Dismiss below">
Awaiting your call <Badge variant="secondary" className="shrink-0">
</Badge> Awaiting your call
</Badge>
</HelpTip>
) : ( ) : (
<Badge variant="outline" className="shrink-0 gap-1"> <HelpTip label="The PR reviewer task hasn't reached completed yet">
<Loader2 className="h-3 w-3 animate-spin" /> <Badge variant="outline" className="shrink-0 gap-1">
Reviewing <Loader2 className="h-3 w-3 animate-spin" />
</Badge> Reviewing
</Badge>
</HelpTip>
)} )}
</div> </div>
{task.description && ( {task.description && (
@@ -224,23 +231,27 @@ export function PrReviewQueue({ className }: PrReviewQueueProps) {
</TooltipProvider> </TooltipProvider>
{awaiting && ( {awaiting && (
<> <>
<Button <HelpTip label="Drops this from the queue — the review stays on the GitHub PR">
variant="outline" <Button
size="sm" variant="outline"
className="text-destructive hover:text-destructive" size="sm"
onClick={() => open(task, "dismiss")} className="text-destructive hover:text-destructive"
> onClick={() => open(task, "dismiss")}
<XCircle className="h-4 w-4 mr-1" /> >
Dismiss <XCircle className="h-4 w-4 mr-1" />
</Button> Dismiss
<Button </Button>
size="sm" </HelpTip>
className="bg-blue-600 hover:bg-blue-700" <HelpTip label="The org takes the contribution over and finishes it">
onClick={() => open(task, "supersede")} <Button
> size="sm"
<Rocket className="h-4 w-4 mr-1" /> className="bg-blue-600 hover:bg-blue-700"
Supersede onClick={() => open(task, "supersede")}
</Button> >
<Rocket className="h-4 w-4 mr-1" />
Supersede
</Button>
</HelpTip>
</> </>
)} )}
</div> </div>
@@ -2,6 +2,7 @@
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import { CreateTaskDialog } from "@/components/tasks/create-task-dialog"; import { CreateTaskDialog } from "@/components/tasks/create-task-dialog";
import { HelpTip } from "@/components/ui/help-tip";
import { Users, BookOpen, Shield, Sparkles, Bot } from "lucide-react"; import { Users, BookOpen, Shield, Sparkles, Bot } from "lucide-react";
import Link from "next/link"; import Link from "next/link";
@@ -11,38 +12,48 @@ export function QuickActionsBar() {
<CreateTaskDialog /> <CreateTaskDialog />
<Link href="/agents" prefetch={false}> <Link href="/agents" prefetch={false}>
<Button variant="outline"> <HelpTip label="Agents page — view the roster and spawn a new agent run">
<Users className="h-4 w-4 mr-2" /> <Button variant="outline">
Spawn Agent <Users className="h-4 w-4 mr-2" />
</Button> Spawn Agent
</Button>
</HelpTip>
</Link> </Link>
<Link href="/prompter" prefetch={false}> <Link href="/prompter" prefetch={false}>
<Button variant="outline"> <HelpTip label="Chat-based interview to draft and submit a new task">
<Sparkles className="h-4 w-4 mr-2" /> <Button variant="outline">
Task Intake <Sparkles className="h-4 w-4 mr-2" />
</Button> Task Intake
</Button>
</HelpTip>
</Link> </Link>
<Link href="/business?tab=secretary" prefetch={false}> <Link href="/business?tab=secretary" prefetch={false}>
<Button variant="outline"> <HelpTip label="Chat with the Secretary — company state and gated CEO directives">
<Bot className="h-4 w-4 mr-2" /> <Button variant="outline">
Secretary <Bot className="h-4 w-4 mr-2" />
</Button> Secretary
</Button>
</HelpTip>
</Link> </Link>
<Link href="/journals" prefetch={false}> <Link href="/journals" prefetch={false}>
<Button variant="outline"> <HelpTip label="Browse agent journal entries and learnings">
<BookOpen className="h-4 w-4 mr-2" /> <Button variant="outline">
View Journals <BookOpen className="h-4 w-4 mr-2" />
</Button> View Journals
</Button>
</HelpTip>
</Link> </Link>
<Link href="/auditor" prefetch={false}> <Link href="/auditor" prefetch={false}>
<Button variant="outline"> <HelpTip label="The Auditor's flagged issues and reports">
<Shield className="h-4 w-4 mr-2" /> <Button variant="outline">
Auditor Report <Shield className="h-4 w-4 mr-2" />
</Button> Auditor Report
</Button>
</HelpTip>
</Link> </Link>
</div> </div>
); );
@@ -4,6 +4,7 @@ import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import { Skeleton } from "@/components/ui/skeleton"; import { Skeleton } from "@/components/ui/skeleton";
import { ScrollArea } from "@/components/ui/scroll-area"; import { ScrollArea } from "@/components/ui/scroll-area";
import { HelpTip } from "@/components/ui/help-tip";
import { Activity as ActivityIcon, ArrowRight } from "lucide-react"; import { Activity as ActivityIcon, ArrowRight } from "lucide-react";
import { ActivityItem, Activity } from "./activity-item"; import { ActivityItem, Activity } from "./activity-item";
import Link from "next/link"; import Link from "next/link";
@@ -20,10 +21,12 @@ export function RecentActivityFeed({
return ( return (
<Card> <Card>
<CardHeader className="pb-3"> <CardHeader className="pb-3">
<CardTitle className="text-lg flex items-center gap-2"> <HelpTip label="The 10 most recent agent actions from the last 24 hours">
<ActivityIcon className="h-5 w-5" /> <CardTitle className="text-lg flex items-center gap-2">
Recent Activity <ActivityIcon className="h-5 w-5" />
</CardTitle> Recent Activity
</CardTitle>
</HelpTip>
</CardHeader> </CardHeader>
<CardContent> <CardContent>
{isLoading ? ( {isLoading ? (
@@ -48,10 +51,12 @@ export function RecentActivityFeed({
)} )}
<div className="mt-4 pt-3 border-t"> <div className="mt-4 pt-3 border-t">
<Link href="/notifications" prefetch={false}> <Link href="/notifications" prefetch={false}>
<Button variant="ghost" size="sm" className="w-full"> <HelpTip label="Opens Notifications — the complete activity history">
View Full Activity <Button variant="ghost" size="sm" className="w-full">
<ArrowRight className="h-4 w-4 ml-2" /> View Full Activity
</Button> <ArrowRight className="h-4 w-4 ml-2" />
</Button>
</HelpTip>
</Link> </Link>
</div> </div>
</CardContent> </CardContent>
@@ -172,7 +172,9 @@ export function ReleaseProposalCard({ className }: { className?: string }) {
<CardTitle className="flex items-center gap-2"> <CardTitle className="flex items-center gap-2">
<Rocket className="h-5 w-5" /> <Rocket className="h-5 w-5" />
Release Proposal Release Proposal
<Badge variant="outline">v{report.proposed_version}</Badge> <HelpTip label="Next version number, computed from the commit classification below">
<Badge variant="outline">v{report.proposed_version}</Badge>
</HelpTip>
<HelpTip label="Semver bump type — how the version number increases (major, minor, or patch)"> <HelpTip label="Semver bump type — how the version number increases (major, minor, or patch)">
<Badge variant="secondary">{report.bump_kind}</Badge> <Badge variant="secondary">{report.bump_kind}</Badge>
</HelpTip> </HelpTip>
@@ -208,18 +210,22 @@ export function ReleaseProposalCard({ className }: { className?: string }) {
)} )}
<div> <div>
<p className="mb-1 text-xs font-semibold uppercase tracking-wide text-muted-foreground"> <HelpTip label="Written by the release manager from conventional-commit messages since the last tag">
Drafted CHANGELOG <p className="mb-1 text-xs font-semibold uppercase tracking-wide text-muted-foreground">
</p> Drafted CHANGELOG
</p>
</HelpTip>
<pre className="max-h-60 overflow-auto rounded-md bg-muted p-3 text-xs whitespace-pre-wrap"> <pre className="max-h-60 overflow-auto rounded-md bg-muted p-3 text-xs whitespace-pre-wrap">
{report.drafted_changelog} {report.drafted_changelog}
</pre> </pre>
</div> </div>
<div> <div>
<p className="mb-1 text-xs font-semibold uppercase tracking-wide text-muted-foreground"> <HelpTip label="Files the executor will rewrite with the new version number on approve">
Version bump plan ({report.version_bump_plan.length} files) <p className="mb-1 text-xs font-semibold uppercase tracking-wide text-muted-foreground">
</p> Version bump plan ({report.version_bump_plan.length} files)
</p>
</HelpTip>
<p className="text-sm text-muted-foreground"> <p className="text-sm text-muted-foreground">
{report.version_bump_plan.join(", ")} {report.version_bump_plan.join(", ")}
</p> </p>
@@ -227,9 +233,11 @@ export function ReleaseProposalCard({ className }: { className?: string }) {
{report.migration_notes.length > 0 && ( {report.migration_notes.length > 0 && (
<div> <div>
<p className="mb-1 text-xs font-semibold uppercase tracking-wide text-muted-foreground"> <HelpTip label="Alembic migrations included in this release — check for a single head">
Migrations <p className="mb-1 text-xs font-semibold uppercase tracking-wide text-muted-foreground">
</p> Migrations
</p>
</HelpTip>
<ul className="space-y-1 text-sm text-muted-foreground"> <ul className="space-y-1 text-sm text-muted-foreground">
{report.migration_notes.map((note, i) => ( {report.migration_notes.map((note, i) => (
<li key={i}>{note}</li> <li key={i}>{note}</li>
@@ -272,25 +280,47 @@ export function ReleaseProposalCard({ className }: { className?: string }) {
)} )}
<div className="flex flex-col-reverse gap-2 pt-1 sm:flex-row sm:items-center sm:justify-end"> <div className="flex flex-col-reverse gap-2 pt-1 sm:flex-row sm:items-center sm:justify-end">
<Button <HelpTip
variant="outline" label={
size="sm" executeInFlight
className="text-destructive hover:text-destructive" ? "Disabled while the execute is running"
onClick={() => setAction("reject")} : "Cancels the proposal — a fresh assessment runs next cycle"
disabled={executeInFlight} }
> >
<XCircle className="mr-1 h-4 w-4" /> <span>
Reject with changes <Button
</Button> variant="outline"
<Button size="sm"
size="sm" className="text-destructive hover:text-destructive"
className="bg-green-600 hover:bg-green-700" onClick={() => setAction("reject")}
onClick={() => setAction("approve")} disabled={executeInFlight}
disabled={executeInFlight} >
<XCircle className="mr-1 h-4 w-4" />
Reject with changes
</Button>
</span>
</HelpTip>
<HelpTip
label={
executeInFlight
? "Disabled while the execute is running"
: "Runs the fail-closed executor: bump + CHANGELOG + gate + CI + publish"
}
> >
<CheckCircle2 className="mr-1 h-4 w-4" /> <span>
{executeFailed ? "Retry approve & publish" : "Approve & publish"} <Button
</Button> size="sm"
className="bg-green-600 hover:bg-green-700"
onClick={() => setAction("approve")}
disabled={executeInFlight}
>
<CheckCircle2 className="mr-1 h-4 w-4" />
{executeFailed
? "Retry approve & publish"
: "Approve & publish"}
</Button>
</span>
</HelpTip>
</div> </div>
</CardContent> </CardContent>
</Card> </Card>
@@ -37,13 +37,19 @@ interface RejectTarget {
function itemStatusBadge(item: RoadmapItem) { function itemStatusBadge(item: RoadmapItem) {
if (item.status === "approved") { if (item.status === "approved") {
return ( return (
<Badge variant="secondary" className="bg-green-600/10 text-green-700"> <HelpTip label="Materialized as a BACKLOG task">
Approved <Badge variant="secondary" className="bg-green-600/10 text-green-700">
</Badge> Approved
</Badge>
</HelpTip>
); );
} }
if (item.status === "rejected") { if (item.status === "rejected") {
return <Badge variant="outline">Rejected</Badge>; return (
<HelpTip label="Recorded, not added to the backlog">
<Badge variant="outline">Rejected</Badge>
</HelpTip>
);
} }
return null; return null;
} }
@@ -95,24 +101,28 @@ function RoadmapItemRow({
)} )}
{isProposed && ( {isProposed && (
<div className="mt-3 flex flex-col-reverse gap-2 sm:flex-row sm:items-center sm:justify-end"> <div className="mt-3 flex flex-col-reverse gap-2 sm:flex-row sm:items-center sm:justify-end">
<Button <HelpTip label="Records your reason and feeds the next cycle's prompt — not added to the backlog">
variant="outline" <Button
size="sm" variant="outline"
className="text-destructive hover:text-destructive" size="sm"
onClick={() => onReject({ taskId, item })} className="text-destructive hover:text-destructive"
> onClick={() => onReject({ taskId, item })}
<XCircle className="mr-1 h-4 w-4" /> >
Reject <XCircle className="mr-1 h-4 w-4" />
</Button> Reject
<Button </Button>
size="sm" </HelpTip>
className="bg-green-600 hover:bg-green-700" <HelpTip label="Materializes this item as a BACKLOG task — needs normal PM activation to start">
disabled={approving} <Button
onClick={() => onApprove(taskId, item.id)} size="sm"
> className="bg-green-600 hover:bg-green-700"
<CheckCircle2 className="mr-1 h-4 w-4" /> disabled={approving}
Approve onClick={() => onApprove(taskId, item.id)}
</Button> >
<CheckCircle2 className="mr-1 h-4 w-4" />
Approve
</Button>
</HelpTip>
</div> </div>
)} )}
</div> </div>
@@ -138,7 +148,9 @@ function RoadmapCycleCard({
<CardTitle className="flex items-center gap-2"> <CardTitle className="flex items-center gap-2">
<Map className="h-5 w-5" /> <Map className="h-5 w-5" />
Roadmap Cycle Roadmap Cycle
<Badge variant="secondary">{pending} pending</Badge> <HelpTip label="Items still awaiting your approve/reject decision">
<Badge variant="secondary">{pending} pending</Badge>
</HelpTip>
</CardTitle> </CardTitle>
<CardDescription>{cycle.goal}</CardDescription> <CardDescription>{cycle.goal}</CardDescription>
</CardHeader> </CardHeader>
@@ -54,17 +54,23 @@ export function ScorecardOverviewPanel() {
<Card> <Card>
<CardHeader className="pb-3"> <CardHeader className="pb-3">
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
<CardTitle className="flex items-center gap-2 text-lg"> <HelpTip label="Org-wide performance rollup for the last 30 days">
<Trophy className="h-5 w-5" /> <CardTitle className="flex items-center gap-2 text-lg">
Performance <Trophy className="h-5 w-5" />
</CardTitle> Performance
</CardTitle>
</HelpTip>
<Link <Link
prefetch={false} prefetch={false}
href="/metrics?tab=scorecards" href="/metrics?tab=scorecards"
className="text-muted-foreground hover:text-foreground flex items-center gap-1 text-xs" className="text-muted-foreground hover:text-foreground flex items-center gap-1 text-xs"
> >
Scorecards <HelpTip label="Per-agent and per-team scorecards, full detail">
<ArrowRight className="h-3 w-3" /> <span className="flex items-center gap-1">
Scorecards
<ArrowRight className="h-3 w-3" />
</span>
</HelpTip>
</Link> </Link>
</div> </div>
</CardHeader> </CardHeader>
@@ -12,6 +12,7 @@ import {
} from "@/components/ui/card"; } from "@/components/ui/card";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import { Badge } from "@/components/ui/badge"; import { Badge } from "@/components/ui/badge";
import { HelpTip } from "@/components/ui/help-tip";
import { Share2 } from "lucide-react"; import { Share2 } from "lucide-react";
// Compact stand-in for the full X/video queues on the command center — the // Compact stand-in for the full X/video queues on the command center — the
@@ -41,7 +42,11 @@ export function SocialSummaryCard({ className }: { className?: string }) {
<CardTitle className="flex items-center gap-2"> <CardTitle className="flex items-center gap-2">
<Share2 className="h-5 w-5" /> <Share2 className="h-5 w-5" />
Social Social
{total > 0 && <Badge variant="secondary">{total}</Badge>} {total > 0 && (
<HelpTip label="X + video drafts combined">
<Badge variant="secondary">{total}</Badge>
</HelpTip>
)}
</CardTitle> </CardTitle>
<CardDescription> <CardDescription>
Held X and video drafts awaiting your approval. Held X and video drafts awaiting your approval.
@@ -49,17 +54,23 @@ export function SocialSummaryCard({ className }: { className?: string }) {
</CardHeader> </CardHeader>
<CardContent className="flex flex-wrap items-center justify-between gap-3"> <CardContent className="flex flex-wrap items-center justify-between gap-3">
<div className="flex gap-4 text-sm text-muted-foreground"> <div className="flex gap-4 text-sm text-muted-foreground">
<span> <HelpTip label="Drafted release posts, feature spotlights, and mention replies awaiting review">
{xCount} X draft{xCount === 1 ? "" : "s"} <span>
</span> {xCount} X draft{xCount === 1 ? "" : "s"}
<span> </span>
{videoCount} video draft{videoCount === 1 ? "" : "s"} </HelpTip>
</span> <HelpTip label="Rendered video clips awaiting review">
<span>
{videoCount} video draft{videoCount === 1 ? "" : "s"}
</span>
</HelpTip>
</div> </div>
<Link href="/social" prefetch={false}> <Link href="/social" prefetch={false}>
<Button variant="outline" size="sm"> <HelpTip label="Opens the full X and video queues plus posting history">
Open Social <Button variant="outline" size="sm">
</Button> Open Social
</Button>
</HelpTip>
</Link> </Link>
</CardContent> </CardContent>
</Card> </Card>
@@ -11,6 +11,7 @@ import {
} from "@/components/ui/card"; } from "@/components/ui/card";
import { Badge } from "@/components/ui/badge"; import { Badge } from "@/components/ui/badge";
import { Skeleton } from "@/components/ui/skeleton"; import { Skeleton } from "@/components/ui/skeleton";
import { HelpTip } from "@/components/ui/help-tip";
import { TrendingUp } from "lucide-react"; import { TrendingUp } from "lucide-react";
interface StrategySignalsPanelProps { interface StrategySignalsPanelProps {
@@ -29,10 +30,12 @@ export function StrategySignalsPanel({ className }: StrategySignalsPanelProps) {
return ( return (
<Card className={className}> <Card className={className}>
<CardHeader> <CardHeader>
<CardTitle className="flex items-center gap-2"> <HelpTip label="Notable patterns the strategy engine detected across delivery data">
<TrendingUp className="h-5 w-5" /> <CardTitle className="flex items-center gap-2">
Strategy Signals <TrendingUp className="h-5 w-5" />
</CardTitle> Strategy Signals
</CardTitle>
</HelpTip>
<CardDescription>Live signals from the strategy engine</CardDescription> <CardDescription>Live signals from the strategy engine</CardDescription>
</CardHeader> </CardHeader>
<CardContent> <CardContent>
@@ -55,9 +58,11 @@ export function StrategySignalsPanel({ className }: StrategySignalsPanelProps) {
> >
<div className="flex-1 min-w-0"> <div className="flex-1 min-w-0">
<div className="flex items-center gap-2 mb-1"> <div className="flex items-center gap-2 mb-1">
<Badge variant="outline" className="text-xs"> <HelpTip label="Signal category from the strategy engine">
{signal.kind} <Badge variant="outline" className="text-xs">
</Badge> {signal.kind}
</Badge>
</HelpTip>
</div> </div>
<p className="font-medium text-sm">{signal.summary}</p> <p className="font-medium text-sm">{signal.summary}</p>
{signal.detail && ( {signal.detail && (
@@ -34,35 +34,41 @@ export function TeamHealthCard({ health }: TeamHealthCardProps) {
</CardHeader> </CardHeader>
<CardContent className="space-y-3"> <CardContent className="space-y-3">
{/* Active Tasks */} {/* Active Tasks */}
<div className="flex items-center justify-between text-sm"> <HelpTip label="Tasks this team currently has claimed or in progress">
<div className="flex items-center gap-2 text-muted-foreground"> <div className="flex items-center justify-between text-sm">
<Users className="h-4 w-4" /> <div className="flex items-center gap-2 text-muted-foreground">
Active <Users className="h-4 w-4" />
Active
</div>
<span className="font-medium">{health.active_tasks}</span>
</div> </div>
<span className="font-medium">{health.active_tasks}</span> </HelpTip>
</div>
{/* Blocked Tasks */} {/* Blocked Tasks */}
<div className="flex items-center justify-between text-sm"> <HelpTip label="Tasks this team currently has in the blocked status">
<div className="flex items-center gap-2 text-muted-foreground"> <div className="flex items-center justify-between text-sm">
<AlertTriangle className="h-4 w-4" /> <div className="flex items-center gap-2 text-muted-foreground">
Blocked <AlertTriangle className="h-4 w-4" />
Blocked
</div>
<span
className={`font-medium ${health.blocked_tasks > 0 ? "text-red-600" : ""}`}
>
{health.blocked_tasks}
</span>
</div> </div>
<span </HelpTip>
className={`font-medium ${health.blocked_tasks > 0 ? "text-red-600" : ""}`}
>
{health.blocked_tasks}
</span>
</div>
{/* Completed This Week */} {/* Completed This Week */}
<div className="flex items-center justify-between text-sm"> <HelpTip label="Tasks this team completed in the last 7 days">
<div className="flex items-center gap-2 text-muted-foreground"> <div className="flex items-center justify-between text-sm">
<TrendingUp className="h-4 w-4" /> <div className="flex items-center gap-2 text-muted-foreground">
Completed (7d) <TrendingUp className="h-4 w-4" />
Completed (7d)
</div>
<span className="font-medium">{health.completed_this_week}</span>
</div> </div>
<span className="font-medium">{health.completed_this_week}</span> </HelpTip>
</div>
{/* Blocked Ratio */} {/* Blocked Ratio */}
{health.blocked_ratio > 0 && ( {health.blocked_ratio > 0 && (
@@ -113,10 +113,12 @@ export function UsageOverviewPanel() {
<Card> <Card>
<CardHeader className="pb-3"> <CardHeader className="pb-3">
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
<CardTitle className="text-lg flex items-center gap-2"> <HelpTip label="Live token/cost across all agent sessions, from /ws/system or a 24h HTTP poll fallback">
<Coins className="h-5 w-5" /> <CardTitle className="text-lg flex items-center gap-2">
Token Usage &amp; Cost <Coins className="h-5 w-5" />
</CardTitle> Token Usage &amp; Cost
</CardTitle>
</HelpTip>
<HelpTip label="Connection to the live /ws/system stream — Polling means it's down and figures refresh via periodic HTTP fetch instead"> <HelpTip label="Connection to the live /ws/system stream — Polling means it's down and figures refresh via periodic HTTP fetch instead">
<Badge className={badge.className}> <Badge className={badge.className}>
{badge.icon} {badge.icon}
@@ -366,15 +366,17 @@ function VideoPostRow({
</div> </div>
<div className="mt-3 flex flex-col-reverse gap-2 sm:flex-row sm:items-center sm:justify-end"> <div className="mt-3 flex flex-col-reverse gap-2 sm:flex-row sm:items-center sm:justify-end">
<Button <HelpTip label="Cancels this draft — it will not be posted">
variant="outline" <Button
size="sm" variant="outline"
className="text-destructive hover:text-destructive" size="sm"
onClick={() => onReject(post)} className="text-destructive hover:text-destructive"
> onClick={() => onReject(post)}
<XCircle className="mr-1 h-4 w-4" /> >
Reject <XCircle className="mr-1 h-4 w-4" />
</Button> Reject
</Button>
</HelpTip>
<HelpTip label={approveHint(approving, overLimit)}> <HelpTip label={approveHint(approving, overLimit)}>
<Button <Button
size="sm" size="sm"
@@ -146,15 +146,17 @@ function XPostRow({
</HelpTip> </HelpTip>
<div className="mt-2 flex flex-col-reverse gap-2 sm:flex-row sm:items-center sm:justify-end"> <div className="mt-2 flex flex-col-reverse gap-2 sm:flex-row sm:items-center sm:justify-end">
<Button <HelpTip label="Cancels this draft — it will not be posted">
variant="outline" <Button
size="sm" variant="outline"
className="text-destructive hover:text-destructive" size="sm"
onClick={() => onReject(post)} className="text-destructive hover:text-destructive"
> onClick={() => onReject(post)}
<XCircle className="mr-1 h-4 w-4" /> >
Reject <XCircle className="mr-1 h-4 w-4" />
</Button> Reject
</Button>
</HelpTip>
<HelpTip <HelpTip
label={approveHint(approving, overLimit, body.trim().length === 0)} label={approveHint(approving, overLimit, body.trim().length === 0)}
> >
+5 -3
View File
@@ -78,9 +78,11 @@ export function AgentItem({
> >
{name} {name}
</p> </p>
<p className="truncate text-xs capitalize text-muted-foreground"> <HelpTip label="Role gates which task types this agent can claim in the lifecycle">
{agent.role.replace(/_/g, " ")} <p className="truncate text-xs capitalize text-muted-foreground">
</p> {agent.role.replace(/_/g, " ")}
</p>
</HelpTip>
</div> </div>
</button> </button>
); );
+11 -6
View File
@@ -2,6 +2,7 @@
import { Agent, Team, AgentRole } from "@/types"; import { Agent, Team, AgentRole } from "@/types";
import { Skeleton } from "@/components/ui/skeleton"; import { Skeleton } from "@/components/ui/skeleton";
import { HelpTip } from "@/components/ui/help-tip";
import { AgentItem } from "./agent-item"; import { AgentItem } from "./agent-item";
interface AgentListProps { interface AgentListProps {
@@ -89,9 +90,11 @@ export function AgentList({
if (!agents || agents.length === 0) { if (!agents || agents.length === 0) {
return ( return (
<div className="p-4 text-center text-muted-foreground text-sm"> <HelpTip label="No agents match the current search, or none are registered yet">
No agents found <div className="p-4 text-center text-muted-foreground text-sm">
</div> No agents found
</div>
</HelpTip>
); );
} }
@@ -104,9 +107,11 @@ export function AgentList({
if (teamAgents.length === 0) return null; if (teamAgents.length === 0) return null;
return ( return (
<div key={teamKey}> <div key={teamKey}>
<h3 className="text-xs font-medium text-muted-foreground uppercase tracking-wider px-2 mb-2"> <HelpTip label="Agents are grouped by team; only teams with at least one agent are shown">
{TEAM_LABELS[teamKey]} <h3 className="text-xs font-medium text-muted-foreground uppercase tracking-wider px-2 mb-2">
</h3> {TEAM_LABELS[teamKey]}
</h3>
</HelpTip>
<div className="space-y-1"> <div className="space-y-1">
{teamAgents.map((agent) => ( {teamAgents.map((agent) => (
<AgentItem <AgentItem
+23 -11
View File
@@ -40,18 +40,24 @@ export function EntryCard({ entry }: EntryCardProps) {
<div className="flex items-start justify-between gap-2 mb-2"> <div className="flex items-start justify-between gap-2 mb-2">
<div className="flex items-center gap-2 flex-wrap"> <div className="flex items-center gap-2 flex-wrap">
<EntryTypeBadge type={entry.type} /> <EntryTypeBadge type={entry.type} />
<span className="text-xs text-muted-foreground flex items-center gap-1"> <HelpTip label={new Date(entry.timestamp).toLocaleString()}>
<Clock className="h-3 w-3" /> <span className="text-xs text-muted-foreground flex items-center gap-1">
{formatTime(entry.timestamp)} <Clock className="h-3 w-3" />
</span> {formatTime(entry.timestamp)}
</span>
</HelpTip>
</div> </div>
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
{entry.sentiment && ( {entry.sentiment && (
<Badge variant="outline" className="text-xs"> <HelpTip label="Agent's self-reported sentiment when writing this entry">
{entry.sentiment} <Badge variant="outline" className="text-xs">
</Badge> {entry.sentiment}
</Badge>
</HelpTip>
)} )}
<ChevronRight className="h-4 w-4 text-muted-foreground opacity-0 group-hover:opacity-100 transition-opacity" /> <HelpTip label="Opens this entry's full detail page">
<ChevronRight className="h-4 w-4 text-muted-foreground opacity-0 group-hover:opacity-100 transition-opacity" />
</HelpTip>
</div> </div>
</div> </div>
@@ -76,9 +82,15 @@ export function EntryCard({ entry }: EntryCardProps) {
</Badge> </Badge>
))} ))}
{entry.tags.length > 3 && ( {entry.tags.length > 3 && (
<span className="text-xs text-muted-foreground"> <HelpTip
+{entry.tags.length - 3} label={`${entry.tags.length - 3} more tag${
</span> entry.tags.length - 3 === 1 ? "" : "s"
} not shown`}
>
<span className="text-xs text-muted-foreground">
+{entry.tags.length - 3}
</span>
</HelpTip>
)} )}
</div> </div>
)} )}
+25 -18
View File
@@ -9,6 +9,7 @@ import {
SelectValue, SelectValue,
} from "@/components/ui/select"; } from "@/components/ui/select";
import { Input } from "@/components/ui/input"; import { Input } from "@/components/ui/input";
import { HelpTip } from "@/components/ui/help-tip";
import { Search, ListTodo } from "lucide-react"; import { Search, ListTodo } from "lucide-react";
interface EntryFilterProps { interface EntryFilterProps {
@@ -43,22 +44,26 @@ export function EntryFilter({
}: EntryFilterProps) { }: EntryFilterProps) {
return ( return (
<div className="flex flex-wrap items-center gap-3"> <div className="flex flex-wrap items-center gap-3">
<div className="relative flex-1 min-w-48"> <HelpTip label="Searches entry titles, content, and tags for this agent">
<Search className="absolute left-3 top-1/2 transform -translate-y-1/2 h-4 w-4 text-muted-foreground" /> <div className="relative flex-1 min-w-48">
<Input <Search className="absolute left-3 top-1/2 transform -translate-y-1/2 h-4 w-4 text-muted-foreground" />
value={searchQuery} <Input
onChange={(e) => onSearchChange(e.target.value)} value={searchQuery}
placeholder="Search entries..." onChange={(e) => onSearchChange(e.target.value)}
className="pl-9" placeholder="Search entries..."
/> className="pl-9"
</div> />
</div>
</HelpTip>
<Select <Select
value={typeFilter} value={typeFilter}
onValueChange={(v) => onTypeChange(v as JournalEntryType | "all")} onValueChange={(v) => onTypeChange(v as JournalEntryType | "all")}
> >
<SelectTrigger className="w-auto min-w-32 shrink-0"> <HelpTip label="Filter entries by type: reflection, decision, learning, struggle, or note">
<SelectValue placeholder="Filter by type" /> <SelectTrigger className="w-auto min-w-32 shrink-0">
</SelectTrigger> <SelectValue placeholder="Filter by type" />
</SelectTrigger>
</HelpTip>
<SelectContent> <SelectContent>
{TYPE_OPTIONS.map((opt) => ( {TYPE_OPTIONS.map((opt) => (
<SelectItem key={opt.value} value={opt.value}> <SelectItem key={opt.value} value={opt.value}>
@@ -71,12 +76,14 @@ export function EntryFilter({
value={taskFilter ?? "all"} value={taskFilter ?? "all"}
onValueChange={(v) => onTaskChange(v === "all" ? null : v)} onValueChange={(v) => onTaskChange(v === "all" ? null : v)}
> >
<SelectTrigger className="w-auto min-w-40 shrink-0"> <HelpTip label="Filter entries to only those linked to a specific task">
<div className="flex items-center gap-2"> <SelectTrigger className="w-auto min-w-40 shrink-0">
<ListTodo className="h-4 w-4" /> <div className="flex items-center gap-2">
<SelectValue placeholder="Filter by task" /> <ListTodo className="h-4 w-4" />
</div> <SelectValue placeholder="Filter by task" />
</SelectTrigger> </div>
</SelectTrigger>
</HelpTip>
<SelectContent> <SelectContent>
<SelectItem value="all">All Tasks</SelectItem> <SelectItem value="all">All Tasks</SelectItem>
{tasksLoading ? ( {tasksLoading ? (
@@ -2,35 +2,49 @@
import { JournalEntryType } from "@/types"; import { JournalEntryType } from "@/types";
import { Badge } from "@/components/ui/badge"; import { Badge } from "@/components/ui/badge";
import { HelpTip } from "@/components/ui/help-tip";
interface EntryTypeBadgeProps { interface EntryTypeBadgeProps {
type: JournalEntryType; type: JournalEntryType;
} }
const typeConfig: Record<JournalEntryType, { label: string; color: string }> = { const typeConfig: Record<
JournalEntryType,
{ label: string; color: string; description: string }
> = {
[JournalEntryType.TASK_REFLECTION]: { [JournalEntryType.TASK_REFLECTION]: {
label: "Task Reflection", label: "Task Reflection",
color: "bg-blue-100 text-blue-700", color: "bg-blue-100 text-blue-700",
description: "A wrap-up reflection the agent wrote after finishing a task",
}, },
[JournalEntryType.DECISION_LOG]: { [JournalEntryType.DECISION_LOG]: {
label: "Decision Log", label: "Decision Log",
color: "bg-purple-100 text-purple-700", color: "bg-purple-100 text-purple-700",
description: "A record of a significant decision the agent made, and why",
}, },
[JournalEntryType.LEARNING]: { [JournalEntryType.LEARNING]: {
label: "Learning", label: "Learning",
color: "bg-green-100 text-green-700", color: "bg-green-100 text-green-700",
description:
"A lesson learned — broadcast to other agents as a knowledge-share notification",
}, },
[JournalEntryType.STRUGGLE]: { [JournalEntryType.STRUGGLE]: {
label: "Struggle", label: "Struggle",
color: "bg-orange-100 text-orange-700", color: "bg-orange-100 text-orange-700",
description: "A difficulty the agent hit; a later entry may mark it resolved",
}, },
[JournalEntryType.GENERAL]: { [JournalEntryType.GENERAL]: {
label: "Note", label: "Note",
color: "bg-gray-100 text-gray-700", color: "bg-gray-100 text-gray-700",
description: "A general note that doesn't fit the other entry types",
}, },
}; };
export function EntryTypeBadge({ type }: EntryTypeBadgeProps) { export function EntryTypeBadge({ type }: EntryTypeBadgeProps) {
const config = typeConfig[type] ?? typeConfig[JournalEntryType.GENERAL]; const config = typeConfig[type] ?? typeConfig[JournalEntryType.GENERAL];
return <Badge className={config.color + " text-xs"}>{config.label}</Badge>; return (
<HelpTip label={config.description}>
<Badge className={config.color + " text-xs"}>{config.label}</Badge>
</HelpTip>
);
} }
@@ -48,59 +48,71 @@ export function GrowthSummary({
return ( return (
<Card> <Card>
<CardHeader className="pb-3"> <CardHeader className="pb-3">
<CardTitle className="text-lg flex items-center gap-2"> <HelpTip label="Aggregated counts and trends from this agent's journal entries">
<TrendingUp className="h-5 w-5" /> <CardTitle className="text-lg flex items-center gap-2">
Growth Summary <TrendingUp className="h-5 w-5" />
</CardTitle> Growth Summary
</CardTitle>
</HelpTip>
</CardHeader> </CardHeader>
<CardContent className="space-y-4"> <CardContent className="space-y-4">
{/* Total Entries */} {/* Total Entries */}
<div className="flex items-center justify-between text-sm"> <HelpTip label="Total journal entries this agent has written across all types">
<span className="text-muted-foreground">Total Entries</span> <div className="flex items-center justify-between text-sm">
<span className="font-medium">{journal.total_entries}</span> <span className="text-muted-foreground">Total Entries</span>
</div> <span className="font-medium">{journal.total_entries}</span>
</div>
</HelpTip>
{/* Entry Types Breakdown */} {/* Entry Types Breakdown */}
<div className="space-y-3"> <div className="space-y-3">
<div className="flex items-center justify-between text-sm"> <HelpTip label="Wrap-up reflections the agent wrote after finishing tasks">
<div className="flex items-center gap-2 text-muted-foreground"> <div className="flex items-center justify-between text-sm">
<BookOpen className="h-4 w-4 text-blue-500" /> <div className="flex items-center gap-2 text-muted-foreground">
Task Reflections <BookOpen className="h-4 w-4 text-blue-500" />
Task Reflections
</div>
<span className="font-medium">
{entries["task_reflection"] || growth?.total_reflections || 0}
</span>
</div> </div>
<span className="font-medium"> </HelpTip>
{entries["task_reflection"] || growth?.total_reflections || 0}
</span>
</div>
<div className="flex items-center justify-between text-sm"> <HelpTip label="Records of decisions the agent made, with the reasoning behind them">
<div className="flex items-center gap-2 text-muted-foreground"> <div className="flex items-center justify-between text-sm">
<GitBranch className="h-4 w-4 text-purple-500" /> <div className="flex items-center gap-2 text-muted-foreground">
Decision Logs <GitBranch className="h-4 w-4 text-purple-500" />
Decision Logs
</div>
<span className="font-medium">
{entries["decision_log"] || growth?.total_decisions || 0}
</span>
</div> </div>
<span className="font-medium"> </HelpTip>
{entries["decision_log"] || growth?.total_decisions || 0}
</span>
</div>
<div className="flex items-center justify-between text-sm"> <HelpTip label="Lessons learned; broadcast to other agents as a knowledge-share notification">
<div className="flex items-center gap-2 text-muted-foreground"> <div className="flex items-center justify-between text-sm">
<Lightbulb className="h-4 w-4 text-green-500" /> <div className="flex items-center gap-2 text-muted-foreground">
Learnings <Lightbulb className="h-4 w-4 text-green-500" />
Learnings
</div>
<span className="font-medium">
{entries["learning"] || growth?.total_learnings || 0}
</span>
</div> </div>
<span className="font-medium"> </HelpTip>
{entries["learning"] || growth?.total_learnings || 0}
</span>
</div>
<div className="flex items-center justify-between text-sm"> <HelpTip label="Difficulties the agent hit; some are later marked resolved">
<div className="flex items-center gap-2 text-muted-foreground"> <div className="flex items-center justify-between text-sm">
<AlertTriangle className="h-4 w-4 text-orange-500" /> <div className="flex items-center gap-2 text-muted-foreground">
Struggles <AlertTriangle className="h-4 w-4 text-orange-500" />
Struggles
</div>
<span className="font-medium">
{entries["struggle"] || growth?.total_struggles || 0}
</span>
</div> </div>
<span className="font-medium"> </HelpTip>
{entries["struggle"] || growth?.total_struggles || 0}
</span>
</div>
</div> </div>
{/* Struggle Resolution Rate */} {/* Struggle Resolution Rate */}
+13 -8
View File
@@ -13,6 +13,7 @@ import { EntryCard } from "./entry-card";
import { EntryFilter } from "./entry-filter"; import { EntryFilter } from "./entry-filter";
import { Skeleton } from "@/components/ui/skeleton"; import { Skeleton } from "@/components/ui/skeleton";
import { ScrollArea } from "@/components/ui/scroll-area"; import { ScrollArea } from "@/components/ui/scroll-area";
import { HelpTip } from "@/components/ui/help-tip";
import { BookOpen, User } from "lucide-react"; import { BookOpen, User } from "lucide-react";
import { getAgentDisplayName } from "@/lib/agent-utils"; import { getAgentDisplayName } from "@/lib/agent-utils";
@@ -99,10 +100,12 @@ export function JournalView({
<h2 className="text-xl font-semibold"> <h2 className="text-xl font-semibold">
{getAgentDisplayName(agent.agent_id)} {getAgentDisplayName(agent.agent_id)}
</h2> </h2>
<p className="text-sm text-muted-foreground capitalize"> <HelpTip label="Role gates claimable task types; team places this agent within the org hierarchy">
{agent.role.replace(/_/g, " ")} -{" "} <p className="text-sm text-muted-foreground capitalize">
{agent.team?.replace(/_/g, " ") || "N/A"} {agent.role.replace(/_/g, " ")} -{" "}
</p> {agent.team?.replace(/_/g, " ") || "N/A"}
</p>
</HelpTip>
</div> </div>
</div> </div>
@@ -118,10 +121,12 @@ export function JournalView({
{/* Entries Section — fills the remaining height; the entries scroll inside it */} {/* Entries Section — fills the remaining height; the entries scroll inside it */}
<div className="flex flex-1 min-h-0 flex-col"> <div className="flex flex-1 min-h-0 flex-col">
<div className="flex items-center justify-between mb-4 shrink-0"> <div className="flex items-center justify-between mb-4 shrink-0">
<h3 className="text-lg font-semibold flex items-center gap-2"> <HelpTip label="Entries below reflect the active type/task filters and search query">
<BookOpen className="h-5 w-5" /> <h3 className="text-lg font-semibold flex items-center gap-2">
Journal Entries <BookOpen className="h-5 w-5" />
</h3> Journal Entries
</h3>
</HelpTip>
</div> </div>
{/* Filter */} {/* Filter */}
+22 -13
View File
@@ -4,6 +4,14 @@ import Link from "next/link";
import { usePathname } from "next/navigation"; import { usePathname } from "next/navigation";
import { LayoutDashboard, ListTodo, Kanban, Sparkles } from "lucide-react"; import { LayoutDashboard, ListTodo, Kanban, Sparkles } from "lucide-react";
import { cn } from "@/lib/utils"; import { cn } from "@/lib/utils";
import { HelpTip } from "@/components/ui/help-tip";
import { navItems } from "./sidebar";
// Reuses navItems' descriptions (defined once in sidebar.tsx) so the two
// nav surfaces never drift out of sync.
function tipFor(href: string): string {
return navItems.find((n) => n.href === href)?.tip ?? "";
}
const BOTTOM_NAV_ITEMS = [ const BOTTOM_NAV_ITEMS = [
{ title: "Overview", href: "/overview", icon: LayoutDashboard }, { title: "Overview", href: "/overview", icon: LayoutDashboard },
@@ -29,19 +37,20 @@ export function BottomTabBar() {
{BOTTOM_NAV_ITEMS.map((item) => { {BOTTOM_NAV_ITEMS.map((item) => {
const isActive = pathname.startsWith(item.href); const isActive = pathname.startsWith(item.href);
return ( return (
<Link <HelpTip key={item.href} label={tipFor(item.href)}>
key={item.href} <Link
href={item.href} href={item.href}
prefetch={false} prefetch={false}
aria-current={isActive ? "page" : undefined} aria-current={isActive ? "page" : undefined}
className={cn( className={cn(
"flex flex-1 flex-col items-center gap-0.5 py-2 text-xs font-medium transition-colors", "flex flex-1 flex-col items-center gap-0.5 py-2 text-xs font-medium transition-colors",
isActive ? "text-primary" : "text-muted-foreground", isActive ? "text-primary" : "text-muted-foreground",
)} )}
> >
<item.icon className="h-5 w-5" /> <item.icon className="h-5 w-5" />
{item.title} {item.title}
</Link> </Link>
</HelpTip>
); );
})} })}
</nav> </nav>
+17 -8
View File
@@ -111,14 +111,23 @@ export function Header() {
<NotificationAlerts /> <NotificationAlerts />
{/* User */} {/* User */}
<div className="flex items-center gap-2 ml-2 pl-4 border-l"> <Tooltip>
<div className="h-8 w-8 rounded-full bg-primary flex items-center justify-center"> <TooltipTrigger asChild>
<span className="text-primary-foreground font-medium text-sm"> <div className="flex items-center gap-2 ml-2 pl-4 border-l">
CEO <div className="h-8 w-8 rounded-full bg-primary flex items-center justify-center">
</span> <span className="text-primary-foreground font-medium text-sm">
</div> CEO
<span className="text-sm font-medium hidden sm:inline">Renzo</span> </span>
</div> </div>
<span className="text-sm font-medium hidden sm:inline">
Renzo
</span>
</div>
</TooltipTrigger>
<TooltipContent>
Signed in as the CEO the panel's single human operator
</TooltipContent>
</Tooltip>
</div> </div>
</header> </header>
); );
+137 -46
View File
@@ -36,29 +36,116 @@ import { useUIStore } from "@/store";
// Flat list, in the exact order product wants the sidebar to read top to // Flat list, in the exact order product wants the sidebar to read top to
// bottom. Notifications lives only in the header's NotificationBell now. // bottom. Notifications lives only in the header's NotificationBell now.
// `tip` doubles as the collapsed icon-rail tooltip and the expanded-row
// hover hint — one description, both surfaces.
export const navItems = [ export const navItems = [
{ title: "Overview", href: "/overview", icon: LayoutDashboard }, {
{ title: "Task Assistant", href: "/prompter", icon: Sparkles }, title: "Overview",
{ title: "Tasks", href: "/tasks", icon: ListTodo }, href: "/overview",
{ title: "Kanban", href: "/kanban", icon: Kanban }, icon: LayoutDashboard,
{ title: "Git", href: "/git", icon: GitBranch }, tip: "Company-wide dashboard: key metrics, blockers, and approval queues",
{ title: "Projects", href: "/projects", icon: FolderGit2 }, },
{ title: "Products", href: "/products", icon: Boxes }, {
{ title: "Social", href: "/social", icon: Share2 }, title: "Task Assistant",
{ title: "Knowledge Base", href: "/knowledge-base", icon: Database }, href: "/prompter",
{ title: "A2A", href: "/a2a", icon: Radio }, icon: Sparkles,
{ title: "Agents", href: "/agents", icon: Bot }, tip: "Chat with Intake to draft and confirm new tasks, including MegaTask batches",
{ title: "Journals", href: "/journals", icon: BookOpen }, },
{ title: "Auditor", href: "/auditor", icon: Shield }, {
{ title: "Metrics", href: "/metrics", icon: Activity }, title: "Tasks",
href: "/tasks",
icon: ListTodo,
tip: "Full task list — filter, search, and open any task's detail",
},
{
title: "Kanban",
href: "/kanban",
icon: Kanban,
tip: "Task board grouped by lifecycle status",
},
{
title: "Git",
href: "/git",
icon: GitBranch,
tip: "Branches, commits, and diffs across every project workspace",
},
{
title: "Projects",
href: "/projects",
icon: FolderGit2,
tip: "Manage repos, git tokens, and per-project settings",
},
{
title: "Products",
href: "/products",
icon: Boxes,
tip: "Products the fleet ships against",
},
{
title: "Social",
href: "/social",
icon: Share2,
tip: "X and TikTok post queues, plus the video pipeline",
},
{
title: "Knowledge Base",
href: "/knowledge-base",
icon: Database,
tip: "Search the RAG corpus — playbooks, learnings, and vault notes",
},
{
title: "A2A",
href: "/a2a",
icon: Radio,
tip: "Live agent-to-agent message switchboard and history",
},
{
title: "Agents",
href: "/agents",
icon: Bot,
tip: "Every agent's live state, spawn controls, and activity stream",
},
{
title: "Journals",
href: "/journals",
icon: BookOpen,
tip: "Per-agent reflections and learnings",
},
{
title: "Auditor",
href: "/auditor",
icon: Shield,
tip: "Silent-observer quality flags and findings review queue",
},
{
title: "Metrics",
href: "/metrics",
icon: Activity,
tip: "Performance, token usage/cost, delivery, and scorecard analytics",
},
]; ];
// Business moved out of the main nav — it lives with the settings-adjacent // Business moved out of the main nav — it lives with the settings-adjacent
// links, separated from navItems by a single Separator (see SidebarFooter). // links, separated from navItems by a single Separator (see SidebarFooter).
const footerItems = [ const footerItems = [
{ title: "Business", href: "/business", icon: Building2 }, {
{ title: "AI Providers", href: "/settings/ai-providers", icon: Cpu }, title: "Business",
{ title: "Settings", href: "/settings", icon: Settings }, href: "/business",
icon: Building2,
tip: "Company goals, roadmap proposals, pitches, and secretary directives",
},
{
title: "AI Providers",
href: "/settings/ai-providers",
icon: Cpu,
tip: "Model routing and per-role provider assignments",
},
{
title: "Settings",
href: "/settings",
icon: Settings,
tip: "Feature flags, credentials, and panel preferences",
},
]; ];
/** /**
@@ -95,16 +182,17 @@ export function SidebarNav({
{!collapsed && <span>{item.title}</span>} {!collapsed && <span>{item.title}</span>}
</Link> </Link>
); );
// Icon-only rail: the label moves into a tooltip. // Collapsed rail: tooltip is the only place the destination is named.
return collapsed ? ( // Expanded row: the label is already visible, so the tooltip instead
// says what lives there — plain Link, safe to wrap (no stateful
// Radix trigger to clobber).
return (
<Tooltip key={item.href}> <Tooltip key={item.href}>
<TooltipTrigger asChild>{link}</TooltipTrigger> <TooltipTrigger asChild>{link}</TooltipTrigger>
<TooltipContent side="right">{item.title}</TooltipContent> <TooltipContent side="right">
{collapsed ? item.title : item.tip}
</TooltipContent>
</Tooltip> </Tooltip>
) : (
<span key={item.href} className="block">
{link}
</span>
); );
})} })}
</nav> </nav>
@@ -144,15 +232,13 @@ export function SidebarFooter({
{!collapsed && <span>{item.title}</span>} {!collapsed && <span>{item.title}</span>}
</Link> </Link>
); );
return collapsed ? ( return (
<Tooltip key={item.href}> <Tooltip key={item.href}>
<TooltipTrigger asChild>{link}</TooltipTrigger> <TooltipTrigger asChild>{link}</TooltipTrigger>
<TooltipContent side="right">{item.title}</TooltipContent> <TooltipContent side="right">
{collapsed ? item.title : item.tip}
</TooltipContent>
</Tooltip> </Tooltip>
) : (
<span key={item.href} className="block">
{link}
</span>
); );
})} })}
</div> </div>
@@ -175,22 +261,27 @@ export function Sidebar() {
{/* Logo */} {/* Logo */}
<div className="flex h-16 items-center justify-between border-b px-4"> <div className="flex h-16 items-center justify-between border-b px-4">
{!sidebarCollapsed && ( {!sidebarCollapsed && (
<Link <Tooltip>
href="/overview" <TooltipTrigger asChild>
className="flex items-center gap-2" <Link
prefetch={false} href="/overview"
> className="flex items-center gap-2"
<Image prefetch={false}
src="/roboco-logo.png" >
alt="RoboCo" <Image
width={32} src="/roboco-logo.png"
height={32} alt="RoboCo"
priority width={32}
unoptimized height={32}
className="h-8 w-8 rounded" priority
/> unoptimized
<span className="font-semibold text-lg">RoboCo</span> className="h-8 w-8 rounded"
</Link> />
<span className="font-semibold text-lg">RoboCo</span>
</Link>
</TooltipTrigger>
<TooltipContent side="bottom">Back to Overview</TooltipContent>
</Tooltip>
)} )}
<TooltipProvider> <TooltipProvider>
<Tooltip> <Tooltip>
@@ -10,6 +10,7 @@ import {
} from "recharts"; } from "recharts";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { Skeleton } from "@/components/ui/skeleton"; import { Skeleton } from "@/components/ui/skeleton";
import { HelpTip } from "@/components/ui/help-tip";
import { useIsMobile } from "@/hooks/use-is-mobile"; import { useIsMobile } from "@/hooks/use-is-mobile";
import type { ModelUsageSlice } from "@/types"; import type { ModelUsageSlice } from "@/types";
@@ -39,7 +40,9 @@ export function ModelUsageDonut({ data, isLoading }: ModelUsageDonutProps) {
return ( return (
<Card> <Card>
<CardHeader className="pb-2"> <CardHeader className="pb-2">
<CardTitle className="text-base">By Model</CardTitle> <HelpTip label="Share of total tokens consumed per model in the selected window — hover a slice for exact tokens and cost">
<CardTitle className="text-base">By Model</CardTitle>
</HelpTip>
</CardHeader> </CardHeader>
<CardContent> <CardContent>
{isLoading ? ( {isLoading ? (
+69 -29
View File
@@ -19,6 +19,7 @@ import { Button } from "@/components/ui/button";
import { Badge } from "@/components/ui/badge"; import { Badge } from "@/components/ui/badge";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { Skeleton } from "@/components/ui/skeleton"; import { Skeleton } from "@/components/ui/skeleton";
import { HelpTip } from "@/components/ui/help-tip";
import { ChevronUp, ChevronDown } from "lucide-react"; import { ChevronUp, ChevronDown } from "lucide-react";
import type { UsageSession } from "@/types"; import type { UsageSession } from "@/types";
@@ -41,17 +42,42 @@ type SortDir = "asc" | "desc";
interface Column { interface Column {
key: SortKey; key: SortKey;
label: string; label: string;
tip: string;
} }
const COLUMNS: Column[] = [ const COLUMNS: Column[] = [
{ key: "agent_slug", label: "Agent" }, { key: "agent_slug", label: "Agent", tip: "The agent slug that ran this session — click to sort" },
{ key: "model", label: "Model" }, { key: "model", label: "Model", tip: "Claude/Grok model used for this session — click to sort" },
{ key: "started_at", label: "Started" }, {
{ key: "total_tokens", label: "Total" }, key: "started_at",
{ key: "tokens_input", label: "Input" }, label: "Started",
{ key: "tokens_output", label: "Output" }, tip: "Session start time, local timezone — click to sort oldest/newest",
{ key: "tokens_cache", label: "Cache" }, },
{ key: "cost", label: "Cost" }, {
key: "total_tokens",
label: "Total",
tip: "Input + output + cache tokens combined — click to sort",
},
{
key: "tokens_input",
label: "Input",
tip: "Prompt tokens sent to the model — click to sort",
},
{
key: "tokens_output",
label: "Output",
tip: "Completion tokens returned by the model — click to sort",
},
{
key: "tokens_cache",
label: "Cache",
tip: "Tokens served from Anthropic's prompt cache — click to sort",
},
{
key: "cost",
label: "Cost",
tip: "Provider-priced dollar cost (local/Ollama sessions are $0) — click to sort",
},
]; ];
function formatTime(ts: string): string { function formatTime(ts: string): string {
@@ -116,7 +142,9 @@ export function SessionsTable({ data, isLoading }: SessionsTableProps) {
return ( return (
<Card> <Card>
<CardHeader className="pb-2"> <CardHeader className="pb-2">
<CardTitle className="text-base">Recent Sessions</CardTitle> <HelpTip label="The most recent agent spawn sessions with their token/cost breakdown — sortable, 10 per page">
<CardTitle className="text-base">Recent Sessions</CardTitle>
</HelpTip>
</CardHeader> </CardHeader>
<CardContent> <CardContent>
{isLoading ? ( {isLoading ? (
@@ -139,8 +167,12 @@ export function SessionsTable({ data, isLoading }: SessionsTableProps) {
className="cursor-pointer select-none text-xs whitespace-nowrap" className="cursor-pointer select-none text-xs whitespace-nowrap"
onClick={() => toggleSort(col.key)} onClick={() => toggleSort(col.key)}
> >
{col.label} <HelpTip label={col.tip}>
<SortIcon col={col.key} /> <span>
{col.label}
<SortIcon col={col.key} />
</span>
</HelpTip>
</TableHead> </TableHead>
))} ))}
</TableRow> </TableRow>
@@ -239,24 +271,32 @@ export function SessionsTable({ data, isLoading }: SessionsTableProps) {
: `${page * PAGE_SIZE + 1}${Math.min((page + 1) * PAGE_SIZE, sorted.length)} of ${sorted.length}`} : `${page * PAGE_SIZE + 1}${Math.min((page + 1) * PAGE_SIZE, sorted.length)} of ${sorted.length}`}
</span> </span>
<div className="flex gap-2"> <div className="flex gap-2">
<Button <HelpTip label={`Show the previous ${PAGE_SIZE} sessions`}>
variant="outline" <span>
size="sm" <Button
onClick={() => setPage((p) => Math.max(0, p - 1))} variant="outline"
disabled={page === 0} size="sm"
> onClick={() => setPage((p) => Math.max(0, p - 1))}
Prev disabled={page === 0}
</Button> >
<Button Prev
variant="outline" </Button>
size="sm" </span>
onClick={() => </HelpTip>
setPage((p) => Math.min(totalPages - 1, p + 1)) <HelpTip label={`Show the next ${PAGE_SIZE} sessions`}>
} <span>
disabled={page >= totalPages - 1} <Button
> variant="outline"
Next size="sm"
</Button> onClick={() =>
setPage((p) => Math.min(totalPages - 1, p + 1))
}
disabled={page >= totalPages - 1}
>
Next
</Button>
</span>
</HelpTip>
</div> </div>
</div> </div>
</> </>
@@ -10,6 +10,7 @@ import {
} from "recharts"; } from "recharts";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { Skeleton } from "@/components/ui/skeleton"; import { Skeleton } from "@/components/ui/skeleton";
import { HelpTip } from "@/components/ui/help-tip";
interface TaskStatusSlice { interface TaskStatusSlice {
name: string; name: string;
@@ -41,7 +42,9 @@ export function TaskStatusChart({ slices, isLoading }: TaskStatusChartProps) {
return ( return (
<Card> <Card>
<CardHeader className="pb-2"> <CardHeader className="pb-2">
<CardTitle className="text-base">Task Status Distribution</CardTitle> <HelpTip label="Live snapshot of how many tasks currently sit in each lifecycle status">
<CardTitle className="text-base">Task Status Distribution</CardTitle>
</HelpTip>
</CardHeader> </CardHeader>
<CardContent> <CardContent>
{isLoading ? ( {isLoading ? (
@@ -12,6 +12,7 @@ import {
} from "recharts"; } from "recharts";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { Skeleton } from "@/components/ui/skeleton"; import { Skeleton } from "@/components/ui/skeleton";
import { HelpTip } from "@/components/ui/help-tip";
import { useIsMobile } from "@/hooks/use-is-mobile"; import { useIsMobile } from "@/hooks/use-is-mobile";
import type { UsageTimePoint } from "@/types"; import type { UsageTimePoint } from "@/types";
@@ -51,7 +52,9 @@ export function UsageTimeSeriesChart({
return ( return (
<Card> <Card>
<CardHeader className="pb-2"> <CardHeader className="pb-2">
<CardTitle className="text-base">Token Usage Over Time</CardTitle> <HelpTip label="Input (prompt) vs. output (completion) tokens per time bucket — hourly or daily, depending on the selected window">
<CardTitle className="text-base">Token Usage Over Time</CardTitle>
</HelpTip>
</CardHeader> </CardHeader>
<CardContent> <CardContent>
{isLoading ? ( {isLoading ? (