feat(panel): dense tooltip pass — work surfaces (270 tips)

Task lifecycle actions get a 25-action tip map, every dialog field and
git panel control annotated; fixes two round-1 disabled-button tips
that could never fire and a double data-state stamp on tab-progress.
This commit is contained in:
Renn F
2026-07-15 18:04:00 +02:00
parent f9dbb30a4e
commit dcd2f84e69
56 changed files with 2554 additions and 1546 deletions
@@ -13,6 +13,7 @@ import {
import { Badge } from "@/components/ui/badge";
import { Search, Filter, X } from "lucide-react";
import { WorkSessionStatus } from "@/types";
import { HelpTip } from "@/components/ui/help-tip";
interface WorkSessionFiltersProps {
searchQuery: string;
@@ -21,10 +22,22 @@ interface WorkSessionFiltersProps {
onStatusChange: (value: WorkSessionStatus[]) => void;
}
const statuses: { value: WorkSessionStatus; label: string }[] = [
{ value: WorkSessionStatus.ACTIVE, label: "Active" },
{ value: WorkSessionStatus.COMPLETED, label: "Completed" },
{ value: WorkSessionStatus.ABANDONED, label: "Abandoned" },
const statuses: { value: WorkSessionStatus; label: string; hint: string }[] = [
{
value: WorkSessionStatus.ACTIVE,
label: "Active",
hint: "An agent is currently working this branch.",
},
{
value: WorkSessionStatus.COMPLETED,
label: "Completed",
hint: "The session's PR was merged — the branch's work is done.",
},
{
value: WorkSessionStatus.ABANDONED,
label: "Abandoned",
hint: "Superseded by a re-claim, cancellation, or project deletion — never an auto-timeout.",
},
];
export function WorkSessionFilters({
@@ -51,29 +64,36 @@ export function WorkSessionFilters({
return (
<div className="flex flex-col gap-4 sm:flex-row sm:items-center">
{/* Search */}
<div className="relative flex-1 max-w-sm">
<Search className="absolute left-3 top-1/2 h-4 w-4 -translate-y-1/2 text-muted-foreground" />
<Input
placeholder="Search by branch name..."
value={searchQuery}
onChange={(e) => onSearchChange(e.target.value)}
className="pl-9"
/>
</div>
<HelpTip label="Filters the table client-side by branch name substring — doesn't re-query the server.">
<div className="relative flex-1 max-w-sm">
<Search className="absolute left-3 top-1/2 h-4 w-4 -translate-y-1/2 text-muted-foreground" />
<Input
placeholder="Search by branch name..."
value={searchQuery}
onChange={(e) => onSearchChange(e.target.value)}
className="pl-9"
/>
</div>
</HelpTip>
{/* Status Filter */}
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant="outline" className="gap-2">
<Filter className="h-4 w-4" />
Status
{statusFilter.length > 0 && (
<Badge variant="secondary" className="ml-1">
{statusFilter.length}
</Badge>
)}
</Button>
</DropdownMenuTrigger>
{/* DropdownMenuTrigger's asChild is safe to wrap directly (unlike
TabsTrigger/CollapsibleTrigger) — it carries no persistent visual
data-state of its own. */}
<HelpTip label="Show only sessions in the checked status(es). No selection shows every session.">
<DropdownMenuTrigger asChild>
<Button variant="outline" className="gap-2">
<Filter className="h-4 w-4" />
Status
{statusFilter.length > 0 && (
<Badge variant="secondary" className="ml-1">
{statusFilter.length}
</Badge>
)}
</Button>
</DropdownMenuTrigger>
</HelpTip>
<DropdownMenuContent align="start" className="w-48">
<DropdownMenuLabel>Filter by Status</DropdownMenuLabel>
<DropdownMenuSeparator />
@@ -83,7 +103,9 @@ export function WorkSessionFilters({
checked={statusFilter.includes(status.value)}
onCheckedChange={() => handleStatusToggle(status.value)}
>
{status.label}
<HelpTip label={status.hint}>
<span className="w-fit">{status.label}</span>
</HelpTip>
</DropdownMenuCheckboxItem>
))}
</DropdownMenuContent>
@@ -91,10 +113,12 @@ export function WorkSessionFilters({
{/* Clear Filters */}
{hasActiveFilters && (
<Button variant="ghost" onClick={clearFilters} className="gap-2">
<X className="h-4 w-4" />
Clear
</Button>
<HelpTip label="Resets the search text and status filter — shows every work session again.">
<Button variant="ghost" onClick={clearFilters} className="gap-2">
<X className="h-4 w-4" />
Clear
</Button>
</HelpTip>
)}
</div>
);
@@ -24,6 +24,7 @@ import {
ResponsiveTableCardRow,
} from "@/components/ui/responsive-table";
import { Skeleton } from "@/components/ui/skeleton";
import { HelpTip } from "@/components/ui/help-tip";
import {
GitBranch,
GitPullRequest,
@@ -40,6 +41,15 @@ interface WorkSessionTableProps {
isLoading: boolean;
}
// Mirrors the status hints in work-session-filters.tsx's dropdown — grounded
// in roboco/services/work_session.py's transition triggers.
const STATUS_HINTS: Record<WorkSessionStatus, string> = {
active: "An agent is currently working this branch.",
completed: "The session's PR was merged — the branch's work is done.",
abandoned:
"Superseded by a re-claim, cancellation, or project deletion — never an auto-timeout.",
};
function getStatusBadge(status: WorkSessionStatus) {
switch (status) {
case "active":
@@ -68,6 +78,29 @@ function getStatusBadge(status: WorkSessionStatus) {
}
}
function StatusBadge({ status }: { status: WorkSessionStatus }) {
return (
<HelpTip label={STATUS_HINTS[status]}>
<span className="w-fit">{getStatusBadge(status)}</span>
</HelpTip>
);
}
function PrBadge({ hasPr }: { hasPr: boolean }) {
return hasPr ? (
<HelpTip label="This session's branch has an open pull request on GitHub.">
<Badge className="bg-purple-500/10 text-purple-500 w-fit">
<GitPullRequest className="h-3 w-3 mr-1" />
PR Open
</Badge>
</HelpTip>
) : (
<HelpTip label="No pull request opened yet for this session's branch.">
<span className="text-sm text-muted-foreground w-fit">No PR</span>
</HelpTip>
);
}
export function WorkSessionTable({
sessions,
isLoading,
@@ -115,42 +148,44 @@ export function WorkSessionTable({
<TableCell>
<div className="flex items-center gap-2">
<GitBranch className="h-4 w-4 text-muted-foreground" />
<Link
prefetch={false}
href={`/work-sessions/${session.id}`}
className="font-medium hover:underline font-mono text-sm"
>
{session.branch_name}
</Link>
<HelpTip label="Open this session's detail page.">
<Link
prefetch={false}
href={`/work-sessions/${session.id}`}
className="font-medium hover:underline font-mono text-sm"
>
{session.branch_name}
</Link>
</HelpTip>
</div>
</TableCell>
<TableCell>
<Link
prefetch={false}
href={`/tasks/${session.task_id}`}
className="text-sm text-muted-foreground hover:text-foreground hover:underline"
title={session.task_id}
>
{session.task_id.slice(0, 8)}...
</Link>
<HelpTip label={`Open task ${session.task_id}`}>
<Link
prefetch={false}
href={`/tasks/${session.task_id}`}
className="text-sm text-muted-foreground hover:text-foreground hover:underline"
>
{session.task_id.slice(0, 8)}...
</Link>
</HelpTip>
</TableCell>
<TableCell>{getStatusBadge(session.status)}</TableCell>
<TableCell>
{session.has_pr ? (
<Badge className="bg-purple-500/10 text-purple-500">
<GitPullRequest className="h-3 w-3 mr-1" />
PR Open
</Badge>
) : (
<span className="text-sm text-muted-foreground">
No PR
</span>
)}
<StatusBadge status={session.status} />
</TableCell>
<TableCell>
<PrBadge hasPr={session.has_pr} />
</TableCell>
<TableCell className="text-sm text-muted-foreground">
{formatDistanceToNow(new Date(session.started_at), {
addSuffix: true,
})}
<HelpTip
label={new Date(session.started_at).toLocaleString()}
>
<span className="w-fit">
{formatDistanceToNow(new Date(session.started_at), {
addSuffix: true,
})}
</span>
</HelpTip>
</TableCell>
<TableCell>
<Tooltip>
@@ -180,14 +215,17 @@ export function WorkSessionTable({
<div className="flex items-start justify-between gap-2">
<div className="flex min-w-0 items-center gap-2">
<GitBranch className="h-4 w-4 shrink-0 text-muted-foreground" />
<Link
prefetch={false}
href={`/work-sessions/${session.id}`}
className="truncate font-mono text-sm font-medium hover:underline"
title={session.branch_name}
<HelpTip
label={`Open this session's detail page — ${session.branch_name}`}
>
{session.branch_name}
</Link>
<Link
prefetch={false}
href={`/work-sessions/${session.id}`}
className="truncate font-mono text-sm font-medium hover:underline"
>
{session.branch_name}
</Link>
</HelpTip>
</div>
<Tooltip>
<TooltipTrigger asChild>
@@ -205,32 +243,32 @@ export function WorkSessionTable({
</div>
<div className="mt-3 divide-y">
<ResponsiveTableCardRow label="Task">
<Link
prefetch={false}
href={`/tasks/${session.task_id}`}
className="text-muted-foreground hover:text-foreground hover:underline"
title={session.task_id}
>
{session.task_id.slice(0, 8)}...
</Link>
<HelpTip label={`Open task ${session.task_id}`}>
<Link
prefetch={false}
href={`/tasks/${session.task_id}`}
className="text-muted-foreground hover:text-foreground hover:underline"
>
{session.task_id.slice(0, 8)}...
</Link>
</HelpTip>
</ResponsiveTableCardRow>
<ResponsiveTableCardRow label="Status">
{getStatusBadge(session.status)}
<StatusBadge status={session.status} />
</ResponsiveTableCardRow>
<ResponsiveTableCardRow label="PR">
{session.has_pr ? (
<Badge className="bg-purple-500/10 text-purple-500">
<GitPullRequest className="h-3 w-3 mr-1" />
PR Open
</Badge>
) : (
<span className="text-muted-foreground">No PR</span>
)}
<PrBadge hasPr={session.has_pr} />
</ResponsiveTableCardRow>
<ResponsiveTableCardRow label="Started">
{formatDistanceToNow(new Date(session.started_at), {
addSuffix: true,
})}
<HelpTip
label={new Date(session.started_at).toLocaleString()}
>
<span className="w-fit">
{formatDistanceToNow(new Date(session.started_at), {
addSuffix: true,
})}
</span>
</HelpTip>
</ResponsiveTableCardRow>
</div>
</ResponsiveTableCard>