W9-5: Tooltip sweep — HelpTip helper + per-view decode (#533)

* [W9-5a] Tooltip sweep foundation: HelpTip helper + shared cryptic badges

HelpTip: DRY wrapper over the verbose 3-element Radix Tooltip pattern so a
broad sweep stays a one-line wrap per site (falsy label short-circuits to the
bare child). Unit-tested (3 cases).

TaskStatusBadge + AgentStateBadge: the panel's most cryptic, most-frequent
elements (15 task lifecycle states, 11 agent states) had no explanation
anywhere. Add a per-state tooltip via HelpTip, with the canonical text in one
description map and exported as taskStatusDescription / agentStateDescription
so the per-view inline renderers (kanban, task header) reuse it instead of
re-declaring. This is part 1 of the W9-5 tooltip sweep; the per-view inline
surfaces follow in subsequent PRs.

* [W9-5b] Per-view tooltip sweep: decode cryptic badges, icon-only buttons, status dots

35 HelpTip additions across 22 panel components, reusing the W9-5a
helper plus taskStatusDescription/agentStateDescription. Tipped:
task-id/commit-hash/branch/PR badges, severity/origin/status badges,
priority (P0-P3), migration/shared flags, MegaTask umbrella badge,
Review Gate / For Resumption / Confidential note badges, icon-only
view/delete/edit/clear/show-hide buttons, semver bump + gate-state
badges, ahead-not-pushed badge. Skipped self-explanatory labeled
buttons and elements already carrying title=.

---------

Co-authored-by: Renn F <rennf93@users.noreply.github.com>
This commit is contained in:
Renzo F
2026-07-15 04:34:17 +02:00
committed by GitHub
co-authored by Renn F
parent f07e2420a8
commit b7f2d84c77
26 changed files with 438 additions and 207 deletions
@@ -8,6 +8,7 @@ import { Input } from "@/components/ui/input";
import { Checkbox } from "@/components/ui/checkbox";
import { Plus, Trash2, Edit3, Check, X } from "lucide-react";
import { toast } from "sonner";
import { HelpTip } from "@/components/ui/help-tip";
import { CollapsibleSection } from "./collapsible-section";
interface AcceptanceCriteriaProps {
@@ -257,22 +258,26 @@ export function AcceptanceCriteria({ task }: AcceptanceCriteriaProps) {
>
{text}
</span>
<Button
size="sm"
variant="ghost"
onClick={() => startEditing(idx)}
className="h-7 w-7 p-0 opacity-0 group-hover:opacity-100 transition-opacity"
>
<Edit3 className="h-3 w-3" />
</Button>
<Button
size="sm"
variant="ghost"
onClick={() => handleDeleteCriterion(idx)}
className="h-7 w-7 p-0 opacity-0 group-hover:opacity-100 transition-opacity text-destructive hover:text-destructive"
>
<Trash2 className="h-3 w-3" />
</Button>
<HelpTip label="Edit this criterion">
<Button
size="sm"
variant="ghost"
onClick={() => startEditing(idx)}
className="h-7 w-7 p-0 opacity-0 group-hover:opacity-100 transition-opacity"
>
<Edit3 className="h-3 w-3" />
</Button>
</HelpTip>
<HelpTip label="Delete this criterion">
<Button
size="sm"
variant="ghost"
onClick={() => handleDeleteCriterion(idx)}
className="h-7 w-7 p-0 opacity-0 group-hover:opacity-100 transition-opacity text-destructive hover:text-destructive"
>
<Trash2 className="h-3 w-3" />
</Button>
</HelpTip>
</>
)}
</li>
@@ -9,6 +9,8 @@ import { ListTree, ExternalLink, Plus } from "lucide-react";
import Link from "next/link";
import { Button } from "@/components/ui/button";
import { getAgentDisplayName } from "@/lib/agent-utils";
import { HelpTip } from "@/components/ui/help-tip";
import { taskStatusDescription } from "@/components/tasks/task-status-badge";
interface SubtasksListProps {
task: Task;
@@ -128,12 +130,14 @@ export function SubtasksList({ task }: SubtasksListProps) {
>
<div className="flex items-center justify-between p-3 rounded-lg border hover:bg-muted/50 transition-colors">
<div className="flex items-center gap-3 min-w-0">
<Badge
variant="secondary"
className={`text-xs shrink-0 ${STATUS_COLORS[subtask.status]}`}
>
{subtask.status.replace(/_/g, " ")}
</Badge>
<HelpTip label={taskStatusDescription(subtask.status)}>
<Badge
variant="secondary"
className={`text-xs shrink-0 ${STATUS_COLORS[subtask.status]}`}
>
{subtask.status.replace(/_/g, " ")}
</Badge>
</HelpTip>
<span className="text-sm truncate">{subtask.title}</span>
</div>
<div className="flex items-center gap-2">
@@ -20,6 +20,7 @@ import {
} from "lucide-react";
import { toast } from "sonner";
import { getAgentDisplayName } from "@/lib/agent-utils";
import { HelpTip } from "@/components/ui/help-tip";
interface TabCommitsProps {
task: Task;
@@ -122,24 +123,28 @@ export function TabCommits({ task }: TabCommitsProps) {
</CardTitle>
<div className="flex items-center justify-center gap-2">
{task.branch_name && (
<Badge
variant="outline"
className="gap-1.5 py-1 px-2.5 font-mono text-sm"
>
<GitBranch className="h-4 w-4" />
{task.branch_name}
</Badge>
<HelpTip label="Git branch this task is being worked on">
<Badge
variant="outline"
className="gap-1.5 py-1 px-2.5 font-mono text-sm"
>
<GitBranch className="h-4 w-4" />
{task.branch_name}
</Badge>
</HelpTip>
)}
{task.pr_url && (
<a href={task.pr_url} target="_blank" rel="noopener noreferrer">
<Badge
variant="secondary"
className="gap-1.5 py-1 px-2.5 hover:bg-secondary/80"
>
PR #{task.pr_number}
<ExternalLink className="h-3.5 w-3.5" />
</Badge>
</a>
<HelpTip label="Pull Request — click to open on GitHub">
<a href={task.pr_url} target="_blank" rel="noopener noreferrer">
<Badge
variant="secondary"
className="gap-1.5 py-1 px-2.5 hover:bg-secondary/80"
>
PR #{task.pr_number}
<ExternalLink className="h-3.5 w-3.5" />
</Badge>
</a>
</HelpTip>
)}
</div>
<div className="flex justify-end">
@@ -243,12 +248,14 @@ export function TabCommits({ task }: TabCommitsProps) {
{/* Meta info */}
<div className="flex items-center gap-3 text-xs text-muted-foreground">
{/* Hash */}
<Badge
variant="outline"
className="font-mono text-xs"
>
{commit.hash.slice(0, 7)}
</Badge>
<HelpTip label="Short git commit hash (first 7 characters)">
<Badge
variant="outline"
className="font-mono text-xs"
>
{commit.hash.slice(0, 7)}
</Badge>
</HelpTip>
{/* Author */}
{commit.author_agent_id && (
@@ -266,14 +273,16 @@ export function TabCommits({ task }: TabCommitsProps) {
</div>
</div>
</div>
<Button
size="sm"
variant="ghost"
onClick={() => handleDelete(commit.hash)}
className="h-7 w-7 p-0 opacity-0 group-hover:opacity-100 text-destructive"
>
<Trash2 className="h-3 w-3" />
</Button>
<HelpTip label="Unlink this commit from the task">
<Button
size="sm"
variant="ghost"
onClick={() => handleDelete(commit.hash)}
className="h-7 w-7 p-0 opacity-0 group-hover:opacity-100 text-destructive"
>
<Trash2 className="h-3 w-3" />
</Button>
</HelpTip>
</div>
</CardContent>
</Card>
@@ -20,6 +20,7 @@ import {
} from "lucide-react";
import Link from "next/link";
import { toast } from "sonner";
import { HelpTip } from "@/components/ui/help-tip";
interface TabDependenciesProps {
task: Task;
@@ -155,27 +156,31 @@ function DependencyList({
className={`flex items-center gap-2 p-3 rounded-lg ${itemBorderClass} ${itemBgClass} transition-colors`}
>
<Link2 className="h-4 w-4 text-muted-foreground shrink-0" />
<Link
href={`/tasks/${depId}`}
className="flex-1"
prefetch={false}
>
<span className="font-mono text-sm hover:underline">
{depId.slice(0, 8)}...
</span>
</Link>
<HelpTip label="Task ID (first 8 characters) — click to open">
<Link
href={`/tasks/${depId}`}
className="flex-1"
prefetch={false}
>
<span className="font-mono text-sm hover:underline">
{depId.slice(0, 8)}...
</span>
</Link>
</HelpTip>
<Badge variant="outline" className={badgeClass}>
{badgeLabel}
</Badge>
<Button
size="sm"
variant="ghost"
onClick={() => handleRemove(depId)}
className="h-7 w-7 p-0 opacity-0 group-hover:opacity-100 transition-opacity text-destructive hover:text-destructive"
disabled={updateTask.isPending}
>
<Trash2 className="h-3 w-3" />
</Button>
<HelpTip label="Remove this dependency">
<Button
size="sm"
variant="ghost"
onClick={() => handleRemove(depId)}
className="h-7 w-7 p-0 opacity-0 group-hover:opacity-100 transition-opacity text-destructive hover:text-destructive"
disabled={updateTask.isPending}
>
<Trash2 className="h-3 w-3" />
</Button>
</HelpTip>
</div>
</li>
))}
@@ -24,6 +24,7 @@ import {
Plus,
} from "lucide-react";
import { toast } from "sonner";
import { HelpTip } from "@/components/ui/help-tip";
interface TabNotesProps {
task: Task;
@@ -47,9 +48,11 @@ function prReviewBadge(task: Task): React.ReactNode {
)?.pr_review?.verdict;
if (!verdict) {
return (
<Badge variant="outline" className="ml-2 text-teal-600 border-teal-300">
Review Gate
</Badge>
<HelpTip label="PR review has not been submitted yet">
<Badge variant="outline" className="ml-2 text-teal-600 border-teal-300">
Review Gate
</Badge>
</HelpTip>
);
}
const map: Record<string, { label: string; cls: string }> = {
@@ -345,9 +348,11 @@ export function TabNotes({ task }: TabNotesProps) {
title="Quick Context"
icon={<FileText className="h-5 w-5" />}
badge={
<Badge variant="outline" className="ml-2">
For Resumption
</Badge>
<HelpTip label="Short context notes for quickly resuming work after an interruption">
<Badge variant="outline" className="ml-2">
For Resumption
</Badge>
</HelpTip>
}
bgClass="bg-blue-50 dark:bg-blue-950 border border-blue-200 dark:border-blue-800"
/>
@@ -420,12 +425,14 @@ export function TabNotes({ task }: TabNotesProps) {
title="Auditor Notes"
icon={<Shield className="h-5 w-5" />}
badge={
<Badge
variant="outline"
className="ml-2 text-purple-600 border-purple-300"
>
Confidential
</Badge>
<HelpTip label="Visible only to the Auditor and CEO">
<Badge
variant="outline"
className="ml-2 text-purple-600 border-purple-300"
>
Confidential
</Badge>
</HelpTip>
}
bgClass="bg-purple-50 dark:bg-purple-950 border border-purple-200 dark:border-purple-800"
/>
@@ -1,5 +1,6 @@
import { TaskStatus } from "@/types";
import { Badge } from "@/components/ui/badge";
import { HelpTip } from "@/components/ui/help-tip";
const statusColors: Record<TaskStatus, string> = {
[TaskStatus.BACKLOG]: "bg-slate-500",
@@ -19,14 +20,44 @@ const statusColors: Record<TaskStatus, string> = {
[TaskStatus.CANCELLED]: "bg-gray-400",
};
const statusDescriptions: Record<TaskStatus, string> = {
[TaskStatus.BACKLOG]: "PM setup phase — dependencies or session not ready yet.",
[TaskStatus.PENDING]: "Ready for work — the orchestrator can spawn an agent.",
[TaskStatus.CLAIMED]: "An agent has locked this task.",
[TaskStatus.IN_PROGRESS]: "Active development in progress.",
[TaskStatus.BLOCKED]: "An external dependency is blocking progress.",
[TaskStatus.PAUSED]: "Temporarily stopped; can resume.",
[TaskStatus.VERIFYING]: "The developer is self-verifying their work.",
[TaskStatus.NEEDS_REVISION]:
"QA / PR-review / PM / CEO requested changes — back with the dev.",
[TaskStatus.AWAITING_QA]: "Submitted for QA review (PR already open).",
[TaskStatus.AWAITING_DOCUMENTATION]:
"Documentation phase — the documenter is writing docs.",
[TaskStatus.AWAITING_PR_REVIEW]:
"PR-review gate: a reviewer checks the assembled PR before the PM merges.",
[TaskStatus.AWAITING_PM_REVIEW]: "Docs complete; the PM is reviewing and merging.",
[TaskStatus.AWAITING_CEO_APPROVAL]: "Escalated for the CEO's final approval.",
[TaskStatus.COMPLETED]: "Terminal — work done and merged.",
[TaskStatus.CANCELLED]: "Terminal — work cancelled.",
};
interface TaskStatusBadgeProps {
status: TaskStatus;
}
/** Plain-language explanation for a task lifecycle state. Reused by the
* shared badge and by inline status renderers (kanban, task header) so the
* canonical text lives in one place. Empty for an unknown status. */
export function taskStatusDescription(status: TaskStatus): string {
return statusDescriptions[status] ?? "";
}
export function TaskStatusBadge({ status }: TaskStatusBadgeProps) {
return (
<Badge className={`${statusColors[status] ?? "bg-slate-600"} text-white`}>
{status.replace(/_/g, " ")}
</Badge>
<HelpTip label={statusDescriptions[status]}>
<Badge className={`${statusColors[status] ?? "bg-slate-600"} text-white`}>
{status.replace(/_/g, " ")}
</Badge>
</HelpTip>
);
}
}
+17 -12
View File
@@ -30,6 +30,7 @@ import {
ResponsiveTableCardEmpty,
} from "@/components/ui/responsive-table";
import { TaskStatusBadge } from "./task-status-badge";
import { HelpTip } from "@/components/ui/help-tip";
import { TaskActions } from "./task-actions";
import { GitStatusBadge } from "./git-status-badge";
import Link from "next/link";
@@ -614,12 +615,14 @@ export function TaskTable({
{task.title}
</span>
{task.batch_id && !task.parent_task_id && (
<Badge
variant="outline"
className="text-xs shrink-0 border-primary/50 text-primary"
>
MegaTask
</Badge>
<HelpTip label="A multi-task batch — this is the umbrella task for a set of related tasks">
<Badge
variant="outline"
className="text-xs shrink-0 border-primary/50 text-primary"
>
MegaTask
</Badge>
</HelpTip>
)}
{childCount > 0 && (
<Badge
@@ -744,12 +747,14 @@ export function TaskTable({
childCount > 0 ? (
<div className="mt-1 flex flex-wrap gap-1">
{task.batch_id && !task.parent_task_id && (
<Badge
variant="outline"
className="border-primary/50 text-xs text-primary"
>
MegaTask
</Badge>
<HelpTip label="A multi-task batch — this is the umbrella task for a set of related tasks">
<Badge
variant="outline"
className="border-primary/50 text-xs text-primary"
>
MegaTask
</Badge>
</HelpTip>
)}
{childCount > 0 && (
<Badge variant="secondary" className="text-xs">