mirror of
https://github.com/rennf93/roboco.git
synced 2026-08-03 07:23:24 +02:00
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:
@@ -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't currently
|
Live status is unavailable — this agent isn'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">
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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 & Cache by Role
|
<Users className="h-4 w-4 text-blue-500" />
|
||||||
</CardTitle>
|
Cost & 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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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">
|
||||||
|
|||||||
@@ -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">
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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't load this conversation</p>
|
<p className="text-sm mb-3">Couldn'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 & journal entries</CardDescription>
|
<CardDescription>Work sessions & 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>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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}
|
||||||
|
|||||||
@@ -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 ? (
|
||||||
|
|||||||
@@ -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 & Start
|
<Rocket className="h-4 w-4 mr-1" />
|
||||||
</Button>
|
Approve & 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 & Cost
|
<Coins className="h-5 w-5" />
|
||||||
</CardTitle>
|
Token Usage & 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)}
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -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>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -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,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 */}
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -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 ? (
|
||||||
|
|||||||
@@ -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 ? (
|
||||||
|
|||||||
Reference in New Issue
Block a user