mirror of
https://github.com/rennf93/roboco.git
synced 2026-08-03 07:23:24 +02:00
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:
@@ -15,6 +15,7 @@ import { cn } from "@/lib/utils";
|
|||||||
import type { A2AChatMessage } from "@/lib/api/a2a";
|
import type { A2AChatMessage } from "@/lib/api/a2a";
|
||||||
import { formatDistanceToNow } from "date-fns";
|
import { formatDistanceToNow } from "date-fns";
|
||||||
import { AlertTriangle, MessagesSquare } from "lucide-react";
|
import { AlertTriangle, MessagesSquare } from "lucide-react";
|
||||||
|
import { HelpTip } from "@/components/ui/help-tip";
|
||||||
|
|
||||||
interface A2ATranscriptProps {
|
interface A2ATranscriptProps {
|
||||||
messages: A2AChatMessage[];
|
messages: A2AChatMessage[];
|
||||||
@@ -227,9 +228,11 @@ export function A2ATranscript({
|
|||||||
{getAgentDisplayName(message.from_agent)}
|
{getAgentDisplayName(message.from_agent)}
|
||||||
</span>
|
</span>
|
||||||
{message.message_kind && (
|
{message.message_kind && (
|
||||||
<Badge variant="outline" className="text-[10px]">
|
<HelpTip label="Type of agent-to-agent message">
|
||||||
{message.message_kind}
|
<Badge variant="outline" className="text-[10px]">
|
||||||
</Badge>
|
{message.message_kind}
|
||||||
|
</Badge>
|
||||||
|
</HelpTip>
|
||||||
)}
|
)}
|
||||||
<span className="text-xs text-muted-foreground ml-auto">
|
<span className="text-xs text-muted-foreground ml-auto">
|
||||||
{formatDistanceToNow(new Date(message.created_at))} ago
|
{formatDistanceToNow(new Date(message.created_at))} ago
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { Badge } from "@/components/ui/badge";
|
import { Badge } from "@/components/ui/badge";
|
||||||
|
import { HelpTip } from "@/components/ui/help-tip";
|
||||||
import {
|
import {
|
||||||
Clock,
|
Clock,
|
||||||
RefreshCw,
|
RefreshCw,
|
||||||
@@ -53,12 +54,32 @@ const stateIcons: Record<string, React.ReactNode> = {
|
|||||||
terminated: <Square className="h-4 w-4" />,
|
terminated: <Square className="h-4 w-4" />,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const stateDescriptions: Record<string, string> = {
|
||||||
|
active: "Agent is actively working a task.",
|
||||||
|
running: "Agent is running — currently executing.",
|
||||||
|
ready: "Agent is ready and waiting for work.",
|
||||||
|
starting: "Agent container is starting up.",
|
||||||
|
idle: "Agent is idle — no task currently claimed.",
|
||||||
|
waiting_long: "Agent has been waiting a long time; may need attention.",
|
||||||
|
paused: "Agent is paused; can resume.",
|
||||||
|
stopped: "Agent container is stopped; can be restarted.",
|
||||||
|
terminated: "Agent container has been terminated.",
|
||||||
|
offline: "Agent is offline.",
|
||||||
|
error: "Agent hit an error state; needs attention.",
|
||||||
|
};
|
||||||
|
|
||||||
interface AgentStateBadgeProps {
|
interface AgentStateBadgeProps {
|
||||||
state: AgentStateString | string;
|
state: AgentStateString | string;
|
||||||
showIcon?: boolean;
|
showIcon?: boolean;
|
||||||
size?: "sm" | "md" | "lg";
|
size?: "sm" | "md" | "lg";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Plain-language explanation for an agent state. Reused by the shared badge
|
||||||
|
* and by inline agent-state renderers. Empty for an unknown state. */
|
||||||
|
export function agentStateDescription(state: string): string {
|
||||||
|
return stateDescriptions[state] ?? "";
|
||||||
|
}
|
||||||
|
|
||||||
export function AgentStateBadge({
|
export function AgentStateBadge({
|
||||||
state,
|
state,
|
||||||
showIcon = true,
|
showIcon = true,
|
||||||
@@ -74,10 +95,12 @@ export function AgentStateBadge({
|
|||||||
const icon = stateIcons[state] || <Square className="h-4 w-4" />;
|
const icon = stateIcons[state] || <Square className="h-4 w-4" />;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Badge className={`${color} text-white ${sizeClasses[size]}`}>
|
<HelpTip label={stateDescriptions[state]}>
|
||||||
{showIcon && <span className="mr-1">{icon}</span>}
|
<Badge className={`${color} text-white ${sizeClasses[size]}`}>
|
||||||
{state.replace(/_/g, " ")}
|
{showIcon && <span className="mr-1">{icon}</span>}
|
||||||
</Badge>
|
{state.replace(/_/g, " ")}
|
||||||
|
</Badge>
|
||||||
|
</HelpTip>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -3,6 +3,7 @@
|
|||||||
import { AuditorFlag, FlagSeverity } from "@/types";
|
import { AuditorFlag, FlagSeverity } from "@/types";
|
||||||
import { Badge } from "@/components/ui/badge";
|
import { Badge } from "@/components/ui/badge";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
|
import { HelpTip } from "@/components/ui/help-tip";
|
||||||
import { Eye, CheckCircle, Send, Clock } from "lucide-react";
|
import { Eye, CheckCircle, Send, Clock } from "lucide-react";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
|
|
||||||
@@ -89,9 +90,11 @@ export function FlaggedItem({
|
|||||||
<div className="flex items-center gap-2 shrink-0">
|
<div className="flex items-center gap-2 shrink-0">
|
||||||
{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}>
|
||||||
<Button variant="ghost" size="sm">
|
<HelpTip label="View related task">
|
||||||
<Eye className="h-4 w-4" />
|
<Button variant="ghost" size="sm">
|
||||||
</Button>
|
<Eye className="h-4 w-4" />
|
||||||
|
</Button>
|
||||||
|
</HelpTip>
|
||||||
</Link>
|
</Link>
|
||||||
)}
|
)}
|
||||||
<Button
|
<Button
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import { Card, CardContent, CardHeader, CardTitle } 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 { Skeleton } from "@/components/ui/skeleton";
|
import { Skeleton } from "@/components/ui/skeleton";
|
||||||
|
import { HelpTip } from "@/components/ui/help-tip";
|
||||||
import { ScrollArea } from "@/components/ui/scroll-area";
|
import { ScrollArea } from "@/components/ui/scroll-area";
|
||||||
import { FileText, Send, Eye, Clock, Plus } from "lucide-react";
|
import { FileText, Send, Eye, Clock, Plus } from "lucide-react";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
@@ -122,9 +123,11 @@ export function ReportsPanel({
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center gap-2 shrink-0">
|
<div className="flex items-center gap-2 shrink-0">
|
||||||
<Button variant="ghost" size="sm">
|
<HelpTip label="View report">
|
||||||
<Eye className="h-4 w-4" />
|
<Button variant="ghost" size="sm">
|
||||||
</Button>
|
<Eye className="h-4 w-4" />
|
||||||
|
</Button>
|
||||||
|
</HelpTip>
|
||||||
{isDraft && (
|
{isDraft && (
|
||||||
<Button
|
<Button
|
||||||
variant="outline"
|
variant="outline"
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import { toast } from "sonner";
|
|||||||
import { Check, Loader2, Send, X } from "lucide-react";
|
import { Check, Loader2, Send, X } from "lucide-react";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
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 { Textarea } from "@/components/ui/textarea";
|
import { Textarea } from "@/components/ui/textarea";
|
||||||
import { Skeleton } from "@/components/ui/skeleton";
|
import { Skeleton } from "@/components/ui/skeleton";
|
||||||
import { OfflineState } from "@/components/ui/offline-state";
|
import { OfflineState } from "@/components/ui/offline-state";
|
||||||
@@ -271,15 +272,17 @@ export function SecretaryTab() {
|
|||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
{sessionId ? (
|
{sessionId ? (
|
||||||
<Button
|
<HelpTip label="Send message">
|
||||||
onClick={() => void handleSend()}
|
<Button
|
||||||
disabled={!input.trim() || streaming}
|
onClick={() => void handleSend()}
|
||||||
size="icon"
|
disabled={!input.trim() || streaming}
|
||||||
className="h-11 w-11 shrink-0"
|
size="icon"
|
||||||
aria-label="Send message"
|
className="h-11 w-11 shrink-0"
|
||||||
>
|
aria-label="Send message"
|
||||||
<Send className="h-4 w-4" />
|
>
|
||||||
</Button>
|
<Send className="h-4 w-4" />
|
||||||
|
</Button>
|
||||||
|
</HelpTip>
|
||||||
) : (
|
) : (
|
||||||
<Button
|
<Button
|
||||||
onClick={() => void handleStart()}
|
onClick={() => void handleStart()}
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ import {
|
|||||||
} from "@/components/ui/card";
|
} from "@/components/ui/card";
|
||||||
import { Badge } from "@/components/ui/badge";
|
import { Badge } from "@/components/ui/badge";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
|
import { HelpTip } from "@/components/ui/help-tip";
|
||||||
import { Input } from "@/components/ui/input";
|
import { Input } from "@/components/ui/input";
|
||||||
import { Switch } from "@/components/ui/switch";
|
import { Switch } from "@/components/ui/switch";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
@@ -395,11 +396,19 @@ export function ConventionsTab({ projectId }: { projectId: string }) {
|
|||||||
</code>{" "}
|
</code>{" "}
|
||||||
<span className="text-muted-foreground">{finding.message}</span>
|
<span className="text-muted-foreground">{finding.message}</span>
|
||||||
</div>
|
</div>
|
||||||
<Badge
|
<HelpTip
|
||||||
variant={finding.level === "block" ? "destructive" : "secondary"}
|
label={
|
||||||
|
finding.level === "block"
|
||||||
|
? "Block-level: refuses the gate"
|
||||||
|
: "Warning: advisory only"
|
||||||
|
}
|
||||||
>
|
>
|
||||||
{finding.rule}
|
<Badge
|
||||||
</Badge>
|
variant={finding.level === "block" ? "destructive" : "secondary"}
|
||||||
|
>
|
||||||
|
{finding.rule}
|
||||||
|
</Badge>
|
||||||
|
</HelpTip>
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
</CardContent>
|
</CardContent>
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import { Button } from "@/components/ui/button";
|
|||||||
import { Skeleton } from "@/components/ui/skeleton";
|
import { Skeleton } from "@/components/ui/skeleton";
|
||||||
import { AlertTriangle, Clock, ArrowRight } from "lucide-react";
|
import { AlertTriangle, Clock, ArrowRight } from "lucide-react";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
|
import { HelpTip } from "@/components/ui/help-tip";
|
||||||
|
|
||||||
interface ActiveBlockersPanelProps {
|
interface ActiveBlockersPanelProps {
|
||||||
tasks: Task[] | undefined;
|
tasks: Task[] | undefined;
|
||||||
@@ -71,9 +72,11 @@ export function ActiveBlockersPanel({
|
|||||||
<span className="text-lg">\uD83D\uDD34</span>
|
<span className="text-lg">\uD83D\uDD34</span>
|
||||||
<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">
|
<HelpTip label="Short task ID — first 8 characters of the full task identifier">
|
||||||
Task #{task.id.slice(0, 8)}
|
<span className="font-medium text-sm truncate">
|
||||||
</span>
|
Task #{task.id.slice(0, 8)}
|
||||||
|
</span>
|
||||||
|
</HelpTip>
|
||||||
<Badge variant="outline" className="text-xs capitalize">
|
<Badge variant="outline" className="text-xs capitalize">
|
||||||
{task.team.replace(/_/g, " ")}
|
{task.team.replace(/_/g, " ")}
|
||||||
</Badge>
|
</Badge>
|
||||||
|
|||||||
@@ -34,6 +34,7 @@ import {
|
|||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import { TaskStatus, Team, type Task } from "@/types";
|
import { TaskStatus, Team, type Task } from "@/types";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
|
import { HelpTip } from "@/components/ui/help-tip";
|
||||||
|
|
||||||
interface CeoApprovalQueueProps {
|
interface CeoApprovalQueueProps {
|
||||||
className?: string;
|
className?: string;
|
||||||
@@ -171,18 +172,24 @@ export function CeoApprovalQueue({ className }: CeoApprovalQueueProps) {
|
|||||||
{
|
{
|
||||||
label: string;
|
label: string;
|
||||||
variant: "default" | "secondary" | "destructive" | "outline";
|
variant: "default" | "secondary" | "destructive" | "outline";
|
||||||
|
desc: string;
|
||||||
}
|
}
|
||||||
> = {
|
> = {
|
||||||
0: { label: "P0", variant: "destructive" },
|
0: { label: "P0", variant: "destructive", desc: "Critical priority — blocks all other work" },
|
||||||
1: { label: "P1", variant: "destructive" },
|
1: { label: "P1", variant: "destructive", desc: "High priority — should be done soon" },
|
||||||
2: { label: "P2", variant: "secondary" },
|
2: { label: "P2", variant: "secondary", desc: "Medium priority — normal scheduling" },
|
||||||
3: { label: "P3", variant: "outline" },
|
3: { label: "P3", variant: "outline", desc: "Low priority — can wait" },
|
||||||
};
|
};
|
||||||
const { label, variant } = variants[priority] || {
|
const { label, variant, desc } = variants[priority] || {
|
||||||
label: `P${priority}`,
|
label: `P${priority}`,
|
||||||
variant: "outline" as const,
|
variant: "outline" as const,
|
||||||
|
desc: `Priority level ${priority}`,
|
||||||
};
|
};
|
||||||
return <Badge variant={variant}>{label}</Badge>;
|
return (
|
||||||
|
<HelpTip label={desc}>
|
||||||
|
<Badge variant={variant}>{label}</Badge>
|
||||||
|
</HelpTip>
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
if (isLoading) {
|
if (isLoading) {
|
||||||
@@ -237,9 +244,11 @@ export function CeoApprovalQueue({ className }: CeoApprovalQueueProps) {
|
|||||||
clipping against it (mirrors the dialog footer's flex-col sm:flex-row). */}
|
clipping against it (mirrors the dialog footer's flex-col sm:flex-row). */}
|
||||||
<div className="flex flex-wrap items-center gap-2 sm:ml-4 sm:shrink-0">
|
<div className="flex flex-wrap items-center gap-2 sm:ml-4 sm:shrink-0">
|
||||||
<Link href={`/tasks/${task.id}`} prefetch={false}>
|
<Link href={`/tasks/${task.id}`} prefetch={false}>
|
||||||
<Button variant="ghost" size="sm">
|
<HelpTip label="View task details">
|
||||||
<FileText className="h-4 w-4" />
|
<Button variant="ghost" size="sm">
|
||||||
</Button>
|
<FileText className="h-4 w-4" />
|
||||||
|
</Button>
|
||||||
|
</HelpTip>
|
||||||
</Link>
|
</Link>
|
||||||
<Button
|
<Button
|
||||||
variant="outline"
|
variant="outline"
|
||||||
|
|||||||
@@ -26,6 +26,7 @@ import { Label } from "@/components/ui/label";
|
|||||||
import { CheckCircle2, XCircle, Rocket, AlertTriangle } from "lucide-react";
|
import { CheckCircle2, XCircle, Rocket, AlertTriangle } from "lucide-react";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
import { usePageRefresh } from "@/hooks";
|
import { usePageRefresh } from "@/hooks";
|
||||||
|
import { HelpTip } from "@/components/ui/help-tip";
|
||||||
|
|
||||||
const _MIN_REJECT_CHARS = 10;
|
const _MIN_REJECT_CHARS = 10;
|
||||||
|
|
||||||
@@ -166,10 +167,14 @@ export function ReleaseProposalCard({ className }: { className?: string }) {
|
|||||||
<Rocket className="h-5 w-5" />
|
<Rocket className="h-5 w-5" />
|
||||||
Release Proposal
|
Release Proposal
|
||||||
<Badge variant="outline">v{report.proposed_version}</Badge>
|
<Badge variant="outline">v{report.proposed_version}</Badge>
|
||||||
<Badge variant="secondary">{report.bump_kind}</Badge>
|
<HelpTip label="Semver bump type — how the version number increases (major, minor, or patch)">
|
||||||
<Badge variant={gateBadgeVariant(report.gate_state)}>
|
<Badge variant="secondary">{report.bump_kind}</Badge>
|
||||||
gate: {report.gate_state}
|
</HelpTip>
|
||||||
</Badge>
|
<HelpTip label="Quality gate status — green means all checks pass, red means failures must be fixed before release">
|
||||||
|
<Badge variant={gateBadgeVariant(report.gate_state)}>
|
||||||
|
gate: {report.gate_state}
|
||||||
|
</Badge>
|
||||||
|
</HelpTip>
|
||||||
</CardTitle>
|
</CardTitle>
|
||||||
<CardDescription>
|
<CardDescription>
|
||||||
{report.change_summary.length} change(s) since the last tag · review
|
{report.change_summary.length} change(s) since the last tag · review
|
||||||
|
|||||||
@@ -25,6 +25,7 @@ import { Textarea } from "@/components/ui/textarea";
|
|||||||
import { Label } from "@/components/ui/label";
|
import { Label } from "@/components/ui/label";
|
||||||
import { CheckCircle2, Map, XCircle } from "lucide-react";
|
import { CheckCircle2, Map, XCircle } from "lucide-react";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
|
import { HelpTip } from "@/components/ui/help-tip";
|
||||||
|
|
||||||
const _MIN_REASON_CHARS = 4;
|
const _MIN_REASON_CHARS = 4;
|
||||||
|
|
||||||
@@ -68,8 +69,12 @@ function RoadmapItemRow({
|
|||||||
<div className="mb-2 flex flex-wrap items-center gap-2">
|
<div className="mb-2 flex flex-wrap items-center gap-2">
|
||||||
<span className="font-medium">{item.title}</span>
|
<span className="font-medium">{item.title}</span>
|
||||||
<Badge variant="outline">{item.team}</Badge>
|
<Badge variant="outline">{item.team}</Badge>
|
||||||
<Badge variant="outline">{item.project_slug}</Badge>
|
<HelpTip label="The project (repository) this roadmap item targets">
|
||||||
<Badge variant="secondary">P{item.priority}</Badge>
|
<Badge variant="outline">{item.project_slug}</Badge>
|
||||||
|
</HelpTip>
|
||||||
|
<HelpTip label={`Priority P${item.priority} — ${item.priority === 0 ? "critical" : item.priority === 1 ? "high" : item.priority === 2 ? "medium" : "low"}`}>
|
||||||
|
<Badge variant="secondary">P{item.priority}</Badge>
|
||||||
|
</HelpTip>
|
||||||
{itemStatusBadge(item)}
|
{itemStatusBadge(item)}
|
||||||
</div>
|
</div>
|
||||||
<p className="text-sm text-muted-foreground">{item.description}</p>
|
<p className="text-sm text-muted-foreground">{item.description}</p>
|
||||||
|
|||||||
@@ -37,6 +37,7 @@ import {
|
|||||||
RefreshCcw,
|
RefreshCcw,
|
||||||
GitGraph,
|
GitGraph,
|
||||||
} from "lucide-react";
|
} from "lucide-react";
|
||||||
|
import { HelpTip } from "@/components/ui/help-tip";
|
||||||
|
|
||||||
interface GitActionsPanelProps {
|
interface GitActionsPanelProps {
|
||||||
status: GitStatusResponse | undefined;
|
status: GitStatusResponse | undefined;
|
||||||
@@ -228,10 +229,12 @@ export function GitActionsPanel({
|
|||||||
)}
|
)}
|
||||||
Push to Remote
|
Push to Remote
|
||||||
{status?.ahead !== undefined && status.ahead > 0 && (
|
{status?.ahead !== undefined && status.ahead > 0 && (
|
||||||
<Badge variant="secondary" className="ml-auto">
|
<HelpTip label="Commits not yet pushed to the remote repository">
|
||||||
<ArrowUp className="h-3 w-3 mr-1" />
|
<Badge variant="secondary" className="ml-auto">
|
||||||
{status.ahead}
|
<ArrowUp className="h-3 w-3 mr-1" />
|
||||||
</Badge>
|
{status.ahead}
|
||||||
|
</Badge>
|
||||||
|
</HelpTip>
|
||||||
)}
|
)}
|
||||||
</Button>
|
</Button>
|
||||||
|
|
||||||
|
|||||||
@@ -23,6 +23,7 @@ import {
|
|||||||
DialogTrigger,
|
DialogTrigger,
|
||||||
} from "@/components/ui/dialog";
|
} from "@/components/ui/dialog";
|
||||||
import { GitBranch, Check, Cloud, Plus, RefreshCw } from "lucide-react";
|
import { GitBranch, Check, Cloud, Plus, RefreshCw } from "lucide-react";
|
||||||
|
import { HelpTip } from "@/components/ui/help-tip";
|
||||||
|
|
||||||
interface GitBranchPanelProps {
|
interface GitBranchPanelProps {
|
||||||
branches: GitBranchListResponse | undefined;
|
branches: GitBranchListResponse | undefined;
|
||||||
@@ -183,9 +184,11 @@ export function GitBranchPanel({
|
|||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
{branch.last_commit && (
|
{branch.last_commit && (
|
||||||
<span className="text-xs text-muted-foreground font-mono shrink-0">
|
<HelpTip label="Short commit hash on this branch">
|
||||||
{branch.last_commit.slice(0, 7)}
|
<span className="text-xs text-muted-foreground font-mono shrink-0">
|
||||||
</span>
|
{branch.last_commit.slice(0, 7)}
|
||||||
|
</span>
|
||||||
|
</HelpTip>
|
||||||
)}
|
)}
|
||||||
</Button>
|
</Button>
|
||||||
))}
|
))}
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import { ScrollArea } from "@/components/ui/scroll-area";
|
|||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
import { GitCommit, User, Calendar } from "lucide-react";
|
import { GitCommit, User, Calendar } from "lucide-react";
|
||||||
import { formatDistanceToNow } from "date-fns";
|
import { formatDistanceToNow } from "date-fns";
|
||||||
|
import { HelpTip } from "@/components/ui/help-tip";
|
||||||
|
|
||||||
interface GitLogPanelProps {
|
interface GitLogPanelProps {
|
||||||
log: GitLogResponse | undefined;
|
log: GitLogResponse | undefined;
|
||||||
@@ -105,12 +106,14 @@ export function GitLogPanel({
|
|||||||
<p className="text-sm font-medium leading-snug line-clamp-2">
|
<p className="text-sm font-medium leading-snug line-clamp-2">
|
||||||
{commit.message}
|
{commit.message}
|
||||||
</p>
|
</p>
|
||||||
<Badge
|
<HelpTip label="Short commit hash">
|
||||||
variant="outline"
|
<Badge
|
||||||
className="font-mono text-xs shrink-0"
|
variant="outline"
|
||||||
>
|
className="font-mono text-xs shrink-0"
|
||||||
{commit.short_hash}
|
>
|
||||||
</Badge>
|
{commit.short_hash}
|
||||||
|
</Badge>
|
||||||
|
</HelpTip>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center gap-3 mt-1 text-xs text-muted-foreground">
|
<div className="flex items-center gap-3 mt-1 text-xs text-muted-foreground">
|
||||||
<span className="flex items-center gap-1">
|
<span className="flex items-center gap-1">
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import { useState, useEffect } from "react";
|
|||||||
import { Input } from "@/components/ui/input";
|
import { Input } from "@/components/ui/input";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import { Search, X, Loader2 } from "lucide-react";
|
import { Search, X, Loader2 } from "lucide-react";
|
||||||
|
import { HelpTip } from "@/components/ui/help-tip";
|
||||||
|
|
||||||
interface KBSearchBarProps {
|
interface KBSearchBarProps {
|
||||||
value: string;
|
value: string;
|
||||||
@@ -64,15 +65,17 @@ export function KBSearchBar({
|
|||||||
className="pl-9 pr-9"
|
className="pl-9 pr-9"
|
||||||
/>
|
/>
|
||||||
{localValue && (
|
{localValue && (
|
||||||
<Button
|
<HelpTip label="Clear search">
|
||||||
variant="ghost"
|
<Button
|
||||||
size="icon-sm"
|
variant="ghost"
|
||||||
onClick={handleClear}
|
size="icon-sm"
|
||||||
aria-label="Clear search"
|
onClick={handleClear}
|
||||||
className="absolute right-1 top-1/2 -translate-y-1/2 text-muted-foreground hover:text-foreground"
|
aria-label="Clear search"
|
||||||
>
|
className="absolute right-1 top-1/2 -translate-y-1/2 text-muted-foreground hover:text-foreground"
|
||||||
<X className="h-4 w-4" />
|
>
|
||||||
</Button>
|
<X className="h-4 w-4" />
|
||||||
|
</Button>
|
||||||
|
</HelpTip>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
{onSearch && (
|
{onSearch && (
|
||||||
|
|||||||
@@ -46,6 +46,7 @@ import {
|
|||||||
} from "lucide-react";
|
} from "lucide-react";
|
||||||
import { OfflineState } from "@/components/ui/offline-state";
|
import { OfflineState } from "@/components/ui/offline-state";
|
||||||
import { Skeleton } from "@/components/ui/skeleton";
|
import { Skeleton } from "@/components/ui/skeleton";
|
||||||
|
import { HelpTip } from "@/components/ui/help-tip";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
import { formatDistanceToNow } from "date-fns";
|
import { formatDistanceToNow } from "date-fns";
|
||||||
import { getErrorMessage } from "@/lib/api/client";
|
import { getErrorMessage } from "@/lib/api/client";
|
||||||
@@ -593,29 +594,33 @@ function KnowledgeBaseBrowserContent() {
|
|||||||
</Badge>
|
</Badge>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<Button
|
<HelpTip label="Refresh this index">
|
||||||
size="sm"
|
<Button
|
||||||
variant="outline"
|
size="sm"
|
||||||
onClick={() =>
|
variant="outline"
|
||||||
handleRefreshIndex(indexType)
|
onClick={() =>
|
||||||
}
|
handleRefreshIndex(indexType)
|
||||||
disabled={refreshIndex.isPending}
|
}
|
||||||
>
|
disabled={refreshIndex.isPending}
|
||||||
{refreshIndex.isPending ? (
|
>
|
||||||
<RefreshCw className="h-3 w-3 animate-spin" />
|
{refreshIndex.isPending ? (
|
||||||
) : (
|
<RefreshCw className="h-3 w-3 animate-spin" />
|
||||||
<RefreshCw className="h-3 w-3" />
|
) : (
|
||||||
)}
|
<RefreshCw className="h-3 w-3" />
|
||||||
</Button>
|
)}
|
||||||
|
</Button>
|
||||||
|
</HelpTip>
|
||||||
<AlertDialog>
|
<AlertDialog>
|
||||||
<AlertDialogTrigger asChild>
|
<AlertDialogTrigger asChild>
|
||||||
<Button
|
<HelpTip label="Delete this index and all its documents">
|
||||||
size="sm"
|
<Button
|
||||||
variant="outline"
|
size="sm"
|
||||||
className="text-red-600"
|
variant="outline"
|
||||||
>
|
className="text-red-600"
|
||||||
<Trash2 className="h-3 w-3" />
|
>
|
||||||
</Button>
|
<Trash2 className="h-3 w-3" />
|
||||||
|
</Button>
|
||||||
|
</HelpTip>
|
||||||
</AlertDialogTrigger>
|
</AlertDialogTrigger>
|
||||||
<AlertDialogContent>
|
<AlertDialogContent>
|
||||||
<AlertDialogHeader>
|
<AlertDialogHeader>
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ import type { BatchProposal, StartRoute } from "@/hooks/use-prompter";
|
|||||||
import type { ProjectSummary } from "@/types";
|
import type { ProjectSummary } from "@/types";
|
||||||
import type { CellWork, DraftProposal } from "@/lib/api/prompter";
|
import type { CellWork, DraftProposal } from "@/lib/api/prompter";
|
||||||
import { Team } from "@/types";
|
import { Team } from "@/types";
|
||||||
|
import { HelpTip } from "@/components/ui/help-tip";
|
||||||
|
|
||||||
/** The delivery cells a multi-cell draft fans out to (one the_work entry each). */
|
/** The delivery cells a multi-cell draft fans out to (one the_work entry each). */
|
||||||
const CELL_TEAMS: Team[] = [Team.BACKEND, Team.FRONTEND, Team.UX_UI];
|
const CELL_TEAMS: Team[] = [Team.BACKEND, Team.FRONTEND, Team.UX_UI];
|
||||||
@@ -158,16 +159,20 @@ export function BatchReviewCard({
|
|||||||
</span>
|
</span>
|
||||||
<div className="flex shrink-0 items-center gap-1">
|
<div className="flex shrink-0 items-center gap-1">
|
||||||
{draft.adds_migration && (
|
{draft.adds_migration && (
|
||||||
<Badge variant="outline" className="gap-1 text-xs">
|
<HelpTip label="This task adds a database migration that must be applied in order">
|
||||||
<Database className="h-3 w-3" />
|
<Badge variant="outline" className="gap-1 text-xs">
|
||||||
migration
|
<Database className="h-3 w-3" />
|
||||||
</Badge>
|
migration
|
||||||
|
</Badge>
|
||||||
|
</HelpTip>
|
||||||
)}
|
)}
|
||||||
{draft.touches_shared && (
|
{draft.touches_shared && (
|
||||||
<Badge variant="outline" className="gap-1 text-xs">
|
<HelpTip label="This task modifies shared code that other tasks may also touch">
|
||||||
<Share2 className="h-3 w-3" />
|
<Badge variant="outline" className="gap-1 text-xs">
|
||||||
shared
|
<Share2 className="h-3 w-3" />
|
||||||
</Badge>
|
shared
|
||||||
|
</Badge>
|
||||||
|
</HelpTip>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -12,6 +12,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 { Badge } from "@/components/ui/badge";
|
import { Badge } from "@/components/ui/badge";
|
||||||
|
import { HelpTip } from "@/components/ui/help-tip";
|
||||||
import {
|
import {
|
||||||
AlertTriangle,
|
AlertTriangle,
|
||||||
CheckCircle2,
|
CheckCircle2,
|
||||||
@@ -194,20 +195,22 @@ export function SelfHostedSection({
|
|||||||
}
|
}
|
||||||
className="pr-10"
|
className="pr-10"
|
||||||
/>
|
/>
|
||||||
<Button
|
<HelpTip label={showToken ? "Hide token" : "Show token"}>
|
||||||
type="button"
|
<Button
|
||||||
variant="ghost"
|
type="button"
|
||||||
size="icon-sm"
|
variant="ghost"
|
||||||
onClick={() => setShowToken((v) => !v)}
|
size="icon-sm"
|
||||||
className="absolute right-1 top-1/2 -translate-y-1/2 text-muted-foreground hover:text-foreground"
|
onClick={() => setShowToken((v) => !v)}
|
||||||
aria-label={showToken ? "Hide token" : "Show token"}
|
className="absolute right-1 top-1/2 -translate-y-1/2 text-muted-foreground hover:text-foreground"
|
||||||
>
|
aria-label={showToken ? "Hide token" : "Show token"}
|
||||||
{showToken ? (
|
>
|
||||||
<EyeOff className="h-4 w-4" />
|
{showToken ? (
|
||||||
) : (
|
<EyeOff className="h-4 w-4" />
|
||||||
<Eye className="h-4 w-4" />
|
) : (
|
||||||
)}
|
<Eye className="h-4 w-4" />
|
||||||
</Button>
|
)}
|
||||||
|
</Button>
|
||||||
|
</HelpTip>
|
||||||
</div>
|
</div>
|
||||||
<Button onClick={handleSave} disabled={saveConfig.isPending}>
|
<Button onClick={handleSave} disabled={saveConfig.isPending}>
|
||||||
{saveConfig.isPending ? "Saving…" : "Save"}
|
{saveConfig.isPending ? "Saving…" : "Save"}
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import { Input } from "@/components/ui/input";
|
|||||||
import { Checkbox } from "@/components/ui/checkbox";
|
import { Checkbox } from "@/components/ui/checkbox";
|
||||||
import { Plus, Trash2, Edit3, Check, X } from "lucide-react";
|
import { Plus, Trash2, Edit3, Check, X } from "lucide-react";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
|
import { HelpTip } from "@/components/ui/help-tip";
|
||||||
import { CollapsibleSection } from "./collapsible-section";
|
import { CollapsibleSection } from "./collapsible-section";
|
||||||
|
|
||||||
interface AcceptanceCriteriaProps {
|
interface AcceptanceCriteriaProps {
|
||||||
@@ -257,22 +258,26 @@ export function AcceptanceCriteria({ task }: AcceptanceCriteriaProps) {
|
|||||||
>
|
>
|
||||||
{text}
|
{text}
|
||||||
</span>
|
</span>
|
||||||
<Button
|
<HelpTip label="Edit this criterion">
|
||||||
size="sm"
|
<Button
|
||||||
variant="ghost"
|
size="sm"
|
||||||
onClick={() => startEditing(idx)}
|
variant="ghost"
|
||||||
className="h-7 w-7 p-0 opacity-0 group-hover:opacity-100 transition-opacity"
|
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>
|
<Edit3 className="h-3 w-3" />
|
||||||
<Button
|
</Button>
|
||||||
size="sm"
|
</HelpTip>
|
||||||
variant="ghost"
|
<HelpTip label="Delete this criterion">
|
||||||
onClick={() => handleDeleteCriterion(idx)}
|
<Button
|
||||||
className="h-7 w-7 p-0 opacity-0 group-hover:opacity-100 transition-opacity text-destructive hover:text-destructive"
|
size="sm"
|
||||||
>
|
variant="ghost"
|
||||||
<Trash2 className="h-3 w-3" />
|
onClick={() => handleDeleteCriterion(idx)}
|
||||||
</Button>
|
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>
|
</li>
|
||||||
|
|||||||
@@ -9,6 +9,8 @@ import { ListTree, ExternalLink, Plus } from "lucide-react";
|
|||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import { getAgentDisplayName } from "@/lib/agent-utils";
|
import { getAgentDisplayName } from "@/lib/agent-utils";
|
||||||
|
import { HelpTip } from "@/components/ui/help-tip";
|
||||||
|
import { taskStatusDescription } from "@/components/tasks/task-status-badge";
|
||||||
|
|
||||||
interface SubtasksListProps {
|
interface SubtasksListProps {
|
||||||
task: Task;
|
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 justify-between p-3 rounded-lg border hover:bg-muted/50 transition-colors">
|
||||||
<div className="flex items-center gap-3 min-w-0">
|
<div className="flex items-center gap-3 min-w-0">
|
||||||
<Badge
|
<HelpTip label={taskStatusDescription(subtask.status)}>
|
||||||
variant="secondary"
|
<Badge
|
||||||
className={`text-xs shrink-0 ${STATUS_COLORS[subtask.status]}`}
|
variant="secondary"
|
||||||
>
|
className={`text-xs shrink-0 ${STATUS_COLORS[subtask.status]}`}
|
||||||
{subtask.status.replace(/_/g, " ")}
|
>
|
||||||
</Badge>
|
{subtask.status.replace(/_/g, " ")}
|
||||||
|
</Badge>
|
||||||
|
</HelpTip>
|
||||||
<span className="text-sm truncate">{subtask.title}</span>
|
<span className="text-sm truncate">{subtask.title}</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ import {
|
|||||||
} from "lucide-react";
|
} from "lucide-react";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
import { getAgentDisplayName } from "@/lib/agent-utils";
|
import { getAgentDisplayName } from "@/lib/agent-utils";
|
||||||
|
import { HelpTip } from "@/components/ui/help-tip";
|
||||||
|
|
||||||
interface TabCommitsProps {
|
interface TabCommitsProps {
|
||||||
task: Task;
|
task: Task;
|
||||||
@@ -122,24 +123,28 @@ export function TabCommits({ task }: TabCommitsProps) {
|
|||||||
</CardTitle>
|
</CardTitle>
|
||||||
<div className="flex items-center justify-center gap-2">
|
<div className="flex items-center justify-center gap-2">
|
||||||
{task.branch_name && (
|
{task.branch_name && (
|
||||||
<Badge
|
<HelpTip label="Git branch this task is being worked on">
|
||||||
variant="outline"
|
<Badge
|
||||||
className="gap-1.5 py-1 px-2.5 font-mono text-sm"
|
variant="outline"
|
||||||
>
|
className="gap-1.5 py-1 px-2.5 font-mono text-sm"
|
||||||
<GitBranch className="h-4 w-4" />
|
>
|
||||||
{task.branch_name}
|
<GitBranch className="h-4 w-4" />
|
||||||
</Badge>
|
{task.branch_name}
|
||||||
|
</Badge>
|
||||||
|
</HelpTip>
|
||||||
)}
|
)}
|
||||||
{task.pr_url && (
|
{task.pr_url && (
|
||||||
<a href={task.pr_url} target="_blank" rel="noopener noreferrer">
|
<HelpTip label="Pull Request — click to open on GitHub">
|
||||||
<Badge
|
<a href={task.pr_url} target="_blank" rel="noopener noreferrer">
|
||||||
variant="secondary"
|
<Badge
|
||||||
className="gap-1.5 py-1 px-2.5 hover:bg-secondary/80"
|
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" />
|
PR #{task.pr_number}
|
||||||
</Badge>
|
<ExternalLink className="h-3.5 w-3.5" />
|
||||||
</a>
|
</Badge>
|
||||||
|
</a>
|
||||||
|
</HelpTip>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<div className="flex justify-end">
|
<div className="flex justify-end">
|
||||||
@@ -243,12 +248,14 @@ export function TabCommits({ task }: TabCommitsProps) {
|
|||||||
{/* Meta info */}
|
{/* Meta info */}
|
||||||
<div className="flex items-center gap-3 text-xs text-muted-foreground">
|
<div className="flex items-center gap-3 text-xs text-muted-foreground">
|
||||||
{/* Hash */}
|
{/* Hash */}
|
||||||
<Badge
|
<HelpTip label="Short git commit hash (first 7 characters)">
|
||||||
variant="outline"
|
<Badge
|
||||||
className="font-mono text-xs"
|
variant="outline"
|
||||||
>
|
className="font-mono text-xs"
|
||||||
{commit.hash.slice(0, 7)}
|
>
|
||||||
</Badge>
|
{commit.hash.slice(0, 7)}
|
||||||
|
</Badge>
|
||||||
|
</HelpTip>
|
||||||
|
|
||||||
{/* Author */}
|
{/* Author */}
|
||||||
{commit.author_agent_id && (
|
{commit.author_agent_id && (
|
||||||
@@ -266,14 +273,16 @@ export function TabCommits({ task }: TabCommitsProps) {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<Button
|
<HelpTip label="Unlink this commit from the task">
|
||||||
size="sm"
|
<Button
|
||||||
variant="ghost"
|
size="sm"
|
||||||
onClick={() => handleDelete(commit.hash)}
|
variant="ghost"
|
||||||
className="h-7 w-7 p-0 opacity-0 group-hover:opacity-100 text-destructive"
|
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>
|
<Trash2 className="h-3 w-3" />
|
||||||
|
</Button>
|
||||||
|
</HelpTip>
|
||||||
</div>
|
</div>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ import {
|
|||||||
} from "lucide-react";
|
} from "lucide-react";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
|
import { HelpTip } from "@/components/ui/help-tip";
|
||||||
|
|
||||||
interface TabDependenciesProps {
|
interface TabDependenciesProps {
|
||||||
task: Task;
|
task: Task;
|
||||||
@@ -155,27 +156,31 @@ function DependencyList({
|
|||||||
className={`flex items-center gap-2 p-3 rounded-lg ${itemBorderClass} ${itemBgClass} transition-colors`}
|
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" />
|
<Link2 className="h-4 w-4 text-muted-foreground shrink-0" />
|
||||||
<Link
|
<HelpTip label="Task ID (first 8 characters) — click to open">
|
||||||
href={`/tasks/${depId}`}
|
<Link
|
||||||
className="flex-1"
|
href={`/tasks/${depId}`}
|
||||||
prefetch={false}
|
className="flex-1"
|
||||||
>
|
prefetch={false}
|
||||||
<span className="font-mono text-sm hover:underline">
|
>
|
||||||
{depId.slice(0, 8)}...
|
<span className="font-mono text-sm hover:underline">
|
||||||
</span>
|
{depId.slice(0, 8)}...
|
||||||
</Link>
|
</span>
|
||||||
|
</Link>
|
||||||
|
</HelpTip>
|
||||||
<Badge variant="outline" className={badgeClass}>
|
<Badge variant="outline" className={badgeClass}>
|
||||||
{badgeLabel}
|
{badgeLabel}
|
||||||
</Badge>
|
</Badge>
|
||||||
<Button
|
<HelpTip label="Remove this dependency">
|
||||||
size="sm"
|
<Button
|
||||||
variant="ghost"
|
size="sm"
|
||||||
onClick={() => handleRemove(depId)}
|
variant="ghost"
|
||||||
className="h-7 w-7 p-0 opacity-0 group-hover:opacity-100 transition-opacity text-destructive hover:text-destructive"
|
onClick={() => handleRemove(depId)}
|
||||||
disabled={updateTask.isPending}
|
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>
|
<Trash2 className="h-3 w-3" />
|
||||||
|
</Button>
|
||||||
|
</HelpTip>
|
||||||
</div>
|
</div>
|
||||||
</li>
|
</li>
|
||||||
))}
|
))}
|
||||||
|
|||||||
@@ -24,6 +24,7 @@ import {
|
|||||||
Plus,
|
Plus,
|
||||||
} from "lucide-react";
|
} from "lucide-react";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
|
import { HelpTip } from "@/components/ui/help-tip";
|
||||||
|
|
||||||
interface TabNotesProps {
|
interface TabNotesProps {
|
||||||
task: Task;
|
task: Task;
|
||||||
@@ -47,9 +48,11 @@ function prReviewBadge(task: Task): React.ReactNode {
|
|||||||
)?.pr_review?.verdict;
|
)?.pr_review?.verdict;
|
||||||
if (!verdict) {
|
if (!verdict) {
|
||||||
return (
|
return (
|
||||||
<Badge variant="outline" className="ml-2 text-teal-600 border-teal-300">
|
<HelpTip label="PR review has not been submitted yet">
|
||||||
Review Gate
|
<Badge variant="outline" className="ml-2 text-teal-600 border-teal-300">
|
||||||
</Badge>
|
Review Gate
|
||||||
|
</Badge>
|
||||||
|
</HelpTip>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
const map: Record<string, { label: string; cls: string }> = {
|
const map: Record<string, { label: string; cls: string }> = {
|
||||||
@@ -345,9 +348,11 @@ export function TabNotes({ task }: TabNotesProps) {
|
|||||||
title="Quick Context"
|
title="Quick Context"
|
||||||
icon={<FileText className="h-5 w-5" />}
|
icon={<FileText className="h-5 w-5" />}
|
||||||
badge={
|
badge={
|
||||||
<Badge variant="outline" className="ml-2">
|
<HelpTip label="Short context notes for quickly resuming work after an interruption">
|
||||||
For Resumption
|
<Badge variant="outline" className="ml-2">
|
||||||
</Badge>
|
For Resumption
|
||||||
|
</Badge>
|
||||||
|
</HelpTip>
|
||||||
}
|
}
|
||||||
bgClass="bg-blue-50 dark:bg-blue-950 border border-blue-200 dark:border-blue-800"
|
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"
|
title="Auditor Notes"
|
||||||
icon={<Shield className="h-5 w-5" />}
|
icon={<Shield className="h-5 w-5" />}
|
||||||
badge={
|
badge={
|
||||||
<Badge
|
<HelpTip label="Visible only to the Auditor and CEO">
|
||||||
variant="outline"
|
<Badge
|
||||||
className="ml-2 text-purple-600 border-purple-300"
|
variant="outline"
|
||||||
>
|
className="ml-2 text-purple-600 border-purple-300"
|
||||||
Confidential
|
>
|
||||||
</Badge>
|
Confidential
|
||||||
|
</Badge>
|
||||||
|
</HelpTip>
|
||||||
}
|
}
|
||||||
bgClass="bg-purple-50 dark:bg-purple-950 border border-purple-200 dark:border-purple-800"
|
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 { TaskStatus } from "@/types";
|
||||||
import { Badge } from "@/components/ui/badge";
|
import { Badge } from "@/components/ui/badge";
|
||||||
|
import { HelpTip } from "@/components/ui/help-tip";
|
||||||
|
|
||||||
const statusColors: Record<TaskStatus, string> = {
|
const statusColors: Record<TaskStatus, string> = {
|
||||||
[TaskStatus.BACKLOG]: "bg-slate-500",
|
[TaskStatus.BACKLOG]: "bg-slate-500",
|
||||||
@@ -19,14 +20,44 @@ const statusColors: Record<TaskStatus, string> = {
|
|||||||
[TaskStatus.CANCELLED]: "bg-gray-400",
|
[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 {
|
interface TaskStatusBadgeProps {
|
||||||
status: TaskStatus;
|
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) {
|
export function TaskStatusBadge({ status }: TaskStatusBadgeProps) {
|
||||||
return (
|
return (
|
||||||
<Badge className={`${statusColors[status] ?? "bg-slate-600"} text-white`}>
|
<HelpTip label={statusDescriptions[status]}>
|
||||||
{status.replace(/_/g, " ")}
|
<Badge className={`${statusColors[status] ?? "bg-slate-600"} text-white`}>
|
||||||
</Badge>
|
{status.replace(/_/g, " ")}
|
||||||
|
</Badge>
|
||||||
|
</HelpTip>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -30,6 +30,7 @@ import {
|
|||||||
ResponsiveTableCardEmpty,
|
ResponsiveTableCardEmpty,
|
||||||
} from "@/components/ui/responsive-table";
|
} from "@/components/ui/responsive-table";
|
||||||
import { TaskStatusBadge } from "./task-status-badge";
|
import { TaskStatusBadge } from "./task-status-badge";
|
||||||
|
import { HelpTip } from "@/components/ui/help-tip";
|
||||||
import { TaskActions } from "./task-actions";
|
import { TaskActions } from "./task-actions";
|
||||||
import { GitStatusBadge } from "./git-status-badge";
|
import { GitStatusBadge } from "./git-status-badge";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
@@ -614,12 +615,14 @@ export function TaskTable({
|
|||||||
{task.title}
|
{task.title}
|
||||||
</span>
|
</span>
|
||||||
{task.batch_id && !task.parent_task_id && (
|
{task.batch_id && !task.parent_task_id && (
|
||||||
<Badge
|
<HelpTip label="A multi-task batch — this is the umbrella task for a set of related tasks">
|
||||||
variant="outline"
|
<Badge
|
||||||
className="text-xs shrink-0 border-primary/50 text-primary"
|
variant="outline"
|
||||||
>
|
className="text-xs shrink-0 border-primary/50 text-primary"
|
||||||
MegaTask
|
>
|
||||||
</Badge>
|
MegaTask
|
||||||
|
</Badge>
|
||||||
|
</HelpTip>
|
||||||
)}
|
)}
|
||||||
{childCount > 0 && (
|
{childCount > 0 && (
|
||||||
<Badge
|
<Badge
|
||||||
@@ -744,12 +747,14 @@ export function TaskTable({
|
|||||||
childCount > 0 ? (
|
childCount > 0 ? (
|
||||||
<div className="mt-1 flex flex-wrap gap-1">
|
<div className="mt-1 flex flex-wrap gap-1">
|
||||||
{task.batch_id && !task.parent_task_id && (
|
{task.batch_id && !task.parent_task_id && (
|
||||||
<Badge
|
<HelpTip label="A multi-task batch — this is the umbrella task for a set of related tasks">
|
||||||
variant="outline"
|
<Badge
|
||||||
className="border-primary/50 text-xs text-primary"
|
variant="outline"
|
||||||
>
|
className="border-primary/50 text-xs text-primary"
|
||||||
MegaTask
|
>
|
||||||
</Badge>
|
MegaTask
|
||||||
|
</Badge>
|
||||||
|
</HelpTip>
|
||||||
)}
|
)}
|
||||||
{childCount > 0 && (
|
{childCount > 0 && (
|
||||||
<Badge variant="secondary" className="text-xs">
|
<Badge variant="secondary" className="text-xs">
|
||||||
|
|||||||
@@ -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(
|
||||||
|
<HelpTip label="reload">
|
||||||
|
<button type="button">Refresh</button>
|
||||||
|
</HelpTip>,
|
||||||
|
);
|
||||||
|
expect(screen.getByText("Refresh")).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("renders the child bare when label is falsy", () => {
|
||||||
|
const { container } = render(
|
||||||
|
<HelpTip label={null}>
|
||||||
|
<button type="button">Refresh</button>
|
||||||
|
</HelpTip>,
|
||||||
|
);
|
||||||
|
// 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(
|
||||||
|
<HelpTip label="">
|
||||||
|
<span>x</span>
|
||||||
|
</HelpTip>,
|
||||||
|
);
|
||||||
|
expect(container.firstChild).toBe(container.querySelector("span"));
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -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 (
|
||||||
|
<Tooltip>
|
||||||
|
<TooltipTrigger asChild>{children}</TooltipTrigger>
|
||||||
|
<TooltipContent side={side}>{label}</TooltipContent>
|
||||||
|
</Tooltip>
|
||||||
|
);
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user