diff --git a/panel/src/components/tasks/task-detail/tab-dependencies.tsx b/panel/src/components/tasks/task-detail/tab-dependencies.tsx
index 78599fd3..27d834d8 100644
--- a/panel/src/components/tasks/task-detail/tab-dependencies.tsx
+++ b/panel/src/components/tasks/task-detail/tab-dependencies.tsx
@@ -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`}
>
-
-
- {depId.slice(0, 8)}...
-
-
+
+
+
+ {depId.slice(0, 8)}...
+
+
+
{badgeLabel}
-
+
+
+
))}
diff --git a/panel/src/components/tasks/task-detail/tab-notes.tsx b/panel/src/components/tasks/task-detail/tab-notes.tsx
index c44f9277..d6b6a736 100644
--- a/panel/src/components/tasks/task-detail/tab-notes.tsx
+++ b/panel/src/components/tasks/task-detail/tab-notes.tsx
@@ -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 (
-
- Review Gate
-
+
+
+ Review Gate
+
+
);
}
const map: Record = {
@@ -345,9 +348,11 @@ export function TabNotes({ task }: TabNotesProps) {
title="Quick Context"
icon={}
badge={
-
- For Resumption
-
+
+
+ For Resumption
+
+
}
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={}
badge={
-
- Confidential
-
+
+
+ Confidential
+
+
}
bgClass="bg-purple-50 dark:bg-purple-950 border border-purple-200 dark:border-purple-800"
/>
diff --git a/panel/src/components/tasks/task-status-badge.tsx b/panel/src/components/tasks/task-status-badge.tsx
index efad1763..754b2da9 100644
--- a/panel/src/components/tasks/task-status-badge.tsx
+++ b/panel/src/components/tasks/task-status-badge.tsx
@@ -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.BACKLOG]: "bg-slate-500",
@@ -19,14 +20,44 @@ const statusColors: Record = {
[TaskStatus.CANCELLED]: "bg-gray-400",
};
+const statusDescriptions: Record = {
+ [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 (
-
- {status.replace(/_/g, " ")}
-
+
+
+ {status.replace(/_/g, " ")}
+
+
);
-}
+}
\ No newline at end of file
diff --git a/panel/src/components/tasks/task-table.tsx b/panel/src/components/tasks/task-table.tsx
index 9e122841..9530cdb9 100644
--- a/panel/src/components/tasks/task-table.tsx
+++ b/panel/src/components/tasks/task-table.tsx
@@ -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}
{task.batch_id && !task.parent_task_id && (
-
- MegaTask
-
+
+
+ MegaTask
+
+
)}
{childCount > 0 && (
0 ? (
{task.batch_id && !task.parent_task_id && (
-
- MegaTask
-
+
+
+ MegaTask
+
+
)}
{childCount > 0 && (
diff --git a/panel/src/components/ui/__tests__/help-tip.test.tsx b/panel/src/components/ui/__tests__/help-tip.test.tsx
new file mode 100644
index 00000000..d0d53f32
--- /dev/null
+++ b/panel/src/components/ui/__tests__/help-tip.test.tsx
@@ -0,0 +1,40 @@
+import { describe, it, expect } from "vitest";
+import { render, screen } from "@testing-library/react";
+import React from "react";
+
+// Radix Tooltip renders content into a portal only when open; for a static
+// render we assert on the trigger passthrough and the short-circuit, not the
+// (hidden) portal content.
+
+import { HelpTip } from "../help-tip";
+
+describe("HelpTip", () => {
+ it("wraps the child so the child still renders", () => {
+ render(
+
+
+ ,
+ );
+ expect(screen.getByText("Refresh")).toBeInTheDocument();
+ });
+
+ it("renders the child bare when label is falsy", () => {
+ const { container } = render(
+
+
+ ,
+ );
+ // No Tooltip wrapper: just the button.
+ expect(container.querySelector("button")).not.toBeNull();
+ expect(container.firstChild).toBe(container.querySelector("button"));
+ });
+
+ it("renders the child bare when label is an empty string", () => {
+ const { container } = render(
+
+ x
+ ,
+ );
+ expect(container.firstChild).toBe(container.querySelector("span"));
+ });
+});
\ No newline at end of file
diff --git a/panel/src/components/ui/help-tip.tsx b/panel/src/components/ui/help-tip.tsx
new file mode 100644
index 00000000..b4230821
--- /dev/null
+++ b/panel/src/components/ui/help-tip.tsx
@@ -0,0 +1,34 @@
+"use client";
+
+import type { ReactNode } from "react";
+import {
+ Tooltip,
+ TooltipContent,
+ TooltipTrigger,
+} from "@/components/ui/tooltip";
+
+interface HelpTipProps {
+ /** The explanation shown on hover/focus. Falsy → renders the child bare. */
+ label: ReactNode;
+ /** Tooltip side relative to the trigger. */
+ side?: "top" | "bottom" | "left" | "right";
+ /** The element to hover; must forward its ref (Button, Badge, span, etc.). */
+ children: ReactNode;
+}
+
+/**
+ * DRY wrapper for the verbose three-element Tooltip pattern. Use for any
+ * element that benefits from a hover/focus explanation — icon-only buttons,
+ * status/severity/origin badges, health dots, metric numbers, abbreviations.
+ * A falsy `label` short-circuits to the bare child, so callers can gate a tip
+ * on whether an explanation is available without conditional markup.
+ */
+export function HelpTip({ label, side = "top", children }: HelpTipProps) {
+ if (!label) return <>{children}>;
+ return (
+
+ {children}
+ {label}
+
+ );
+}
\ No newline at end of file