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
+11 -1
View File
@@ -3,6 +3,7 @@
import { useEffect, useRef } from "react"; import { useEffect, useRef } from "react";
import { Loader2, Sparkles, X } from "lucide-react"; import { Loader2, Sparkles, X } from "lucide-react";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import { HelpTip } from "@/components/ui/help-tip";
import { usePrompter } from "@/hooks/use-prompter"; import { usePrompter } from "@/hooks/use-prompter";
import { Team } from "@/types"; import { Team } from "@/types";
import { import {
@@ -77,16 +78,25 @@ export default function PrompterPage() {
</div> </div>
{/* End chat — reap the agent and return to the form (any chat state) */} {/* End chat — reap the agent and return to the form (any chat state) */}
{!showForm && state !== "success" && ( {!showForm && state !== "success" && (
<HelpTip label="Stops the agent session and clears this chat — you'll return to the intake form">
{/* disabled sets pointer-events:none on the Button, which would
swallow hover — the tip sits on a wrapping span instead. */}
<span
className="ml-auto"
tabIndex={state === "launching" ? undefined : 0}
>
<Button <Button
variant="ghost" variant="ghost"
size="sm" size="sm"
className="ml-auto text-muted-foreground" className="text-muted-foreground"
onClick={startAnother} onClick={startAnother}
disabled={state === "launching"} disabled={state === "launching"}
> >
<X className="mr-1 h-4 w-4" /> <X className="mr-1 h-4 w-4" />
End chat End chat
</Button> </Button>
</span>
</HelpTip>
)} )}
</div> </div>
@@ -29,6 +29,7 @@ import { AlertTriangle, ArrowLeft } from "lucide-react";
import Link from "next/link"; import Link from "next/link";
import { useRouter } from "next/navigation"; import { useRouter } from "next/navigation";
import { toast } from "sonner"; import { toast } from "sonner";
import { HelpTip } from "@/components/ui/help-tip";
interface TaskDetailPageProps { interface TaskDetailPageProps {
params: Promise<{ taskId: string }>; params: Promise<{ taskId: string }>;
@@ -415,12 +416,14 @@ export default function TaskDetailPage({ params }: TaskDetailPageProps) {
if (error || !task) { if (error || !task) {
return ( return (
<div className="space-y-6"> <div className="space-y-6">
<HelpTip label="Returns to the task list, preserving whatever filters/sort/page you had set there.">
<Link href="/tasks" prefetch={false}> <Link href="/tasks" prefetch={false}>
<Button variant="ghost" size="sm"> <Button variant="ghost" size="sm">
<ArrowLeft className="h-4 w-4 mr-2" /> <ArrowLeft className="h-4 w-4 mr-2" />
Back to Tasks Back to Tasks
</Button> </Button>
</Link> </Link>
</HelpTip>
<Card> <Card>
<CardContent className="pt-6"> <CardContent className="pt-6">
@@ -432,9 +435,11 @@ export default function TaskDetailPage({ params }: TaskDetailPageProps) {
"The task you're looking for doesn't exist or has been deleted."} "The task you're looking for doesn't exist or has been deleted."}
</p> </p>
<div className="flex justify-center gap-4"> <div className="flex justify-center gap-4">
<HelpTip label="Leaves this not-found error and returns to the full task list.">
<Link href="/tasks" prefetch={false}> <Link href="/tasks" prefetch={false}>
<Button>View All Tasks</Button> <Button>View All Tasks</Button>
</Link> </Link>
</HelpTip>
</div> </div>
</div> </div>
</CardContent> </CardContent>
@@ -474,12 +479,14 @@ export default function TaskDetailPage({ params }: TaskDetailPageProps) {
task.team !== Team.MAIN_PM && ( task.team !== Team.MAIN_PM && (
<div className="flex justify-end gap-2"> <div className="flex justify-end gap-2">
{task.team === Team.BOARD && ( {task.team === Team.BOARD && (
<HelpTip label="Re-opens the intake chat, threading in the board's review notes so the draft can be revised.">
<Button <Button
variant="outline" variant="outline"
onClick={() => router.push(`/prompter?redraft=${task.id}`)} onClick={() => router.push(`/prompter?redraft=${task.id}`)}
> >
Re-draft with board feedback Re-draft with board feedback
</Button> </Button>
</HelpTip>
)} )}
<ApproveAndStartButton task={task} /> <ApproveAndStartButton task={task} />
</div> </div>
+3
View File
@@ -18,6 +18,7 @@ import type { TaskFilters as TaskApiFilters } from "@/lib/api/tasks";
import { Skeleton } from "@/components/ui/skeleton"; import { Skeleton } from "@/components/ui/skeleton";
import { usePageRefresh } from "@/hooks"; import { usePageRefresh } from "@/hooks";
import { useScrollRestorationStore } from "@/lib/stores"; import { useScrollRestorationStore } from "@/lib/stores";
import { HelpTip } from "@/components/ui/help-tip";
function TasksPageContent() { function TasksPageContent() {
const router = useRouter(); const router = useRouter();
@@ -304,9 +305,11 @@ function TasksPageContent() {
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
<div> <div>
<h1 className="text-3xl font-bold tracking-tight">Tasks</h1> <h1 className="text-3xl font-bold tracking-tight">Tasks</h1>
<HelpTip label="Filters, sort, and pagination all persist in the URL — this view is bookmarkable and shareable.">
<p className="text-muted-foreground"> <p className="text-muted-foreground">
Manage and track all tasks across teams Manage and track all tasks across teams
</p> </p>
</HelpTip>
</div> </div>
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<CreateTaskDialog /> <CreateTaskDialog />
@@ -71,6 +71,9 @@ describe("CodeSnippet", () => {
// The active line's content is "b"; its row carries the highlight class. // The active line's content is "b"; its row carries the highlight class.
const row = screen.getByText("b").parentElement; const row = screen.getByText("b").parentElement;
expect(row?.className).toContain("bg-blue-500/15"); expect(row?.className).toContain("bg-blue-500/15");
// The active line's number is now a real HelpTip trigger (Radix injects
// data-state via asChild), not a bare span.
expect(screen.getByText("2").getAttribute("data-state")).toBe("closed");
}); });
it("renders the fail-open hint on an error", () => { it("renders the fail-open hint on an error", () => {
+8 -3
View File
@@ -4,6 +4,7 @@ import { useGitFile } from "@/hooks/use-git";
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 { cn } from "@/lib/utils"; import { cn } from "@/lib/utils";
import { HelpTip } from "@/components/ui/help-tip";
interface CodeSnippetProps { interface CodeSnippetProps {
branch: string | null | undefined; branch: string | null | undefined;
@@ -64,9 +65,11 @@ export function CodeSnippet({
"bg-blue-500/15 ring-1 ring-inset ring-blue-500/30", "bg-blue-500/15 ring-1 ring-inset ring-blue-500/30",
)} )}
> >
<HelpTip label={isActive ? "The line flagged in this finding" : ""}>
<span className="select-none w-8 shrink-0 text-right text-muted-foreground/60"> <span className="select-none w-8 shrink-0 text-right text-muted-foreground/60">
{lineNo} {lineNo}
</span> </span>
</HelpTip>
<span className="whitespace-pre">{text || " "}</span> <span className="whitespace-pre">{text || " "}</span>
</div> </div>
); );
@@ -74,10 +77,12 @@ export function CodeSnippet({
</pre> </pre>
</ScrollArea> </ScrollArea>
{data.truncated && ( {data.truncated && (
<p className="border-t px-2 py-1 text-[10px] text-muted-foreground"> <HelpTip label="Only a window around the flagged line is loaded, not the whole file — widen `context` to see more.">
showing lines {data.start_line}{data.start_line + lines.length - 1}{" "} <p className="border-t px-2 py-1 text-[10px] text-muted-foreground w-fit">
of {data.total_lines} showing lines {data.start_line}
{data.start_line + lines.length - 1} of {data.total_lines}
</p> </p>
</HelpTip>
)} )}
</div> </div>
); );
+87 -6
View File
@@ -149,6 +149,23 @@ export function GitActionsPanel({
<Dialog <Dialog
open={showCommitDialog} open={showCommitDialog}
onOpenChange={handleCommitDialogOpenChange} onOpenChange={handleCommitDialogOpenChange}
>
{/* HelpTip+span wraps the whole DialogTrigger, not the Button
itself — the Button stays DialogTrigger's asChild target so
Radix's Slot merge is undisturbed, and the outer span keeps the
tip hoverable even while disabled (disabled sets
pointer-events:none on the button, which would swallow hover on
anything nested inside it). */}
<HelpTip
label={
hasStagedChanges
? "Records the staged files as a local commit — doesn't push to the remote."
: "Nothing staged yet — stage files first before a commit can be made."
}
>
<span
className="block w-full"
tabIndex={!hasStagedChanges ? 0 : undefined}
> >
<DialogTrigger asChild> <DialogTrigger asChild>
<Button <Button
@@ -165,13 +182,19 @@ export function GitActionsPanel({
)} )}
</Button> </Button>
</DialogTrigger> </DialogTrigger>
</span>
</HelpTip>
<DialogContent> <DialogContent>
<DialogHeader> <DialogHeader>
<DialogTitle>Commit Changes</DialogTitle> <DialogTitle>Commit Changes</DialogTitle>
</DialogHeader> </DialogHeader>
<div className="space-y-4 py-4"> <div className="space-y-4 py-4">
<div className="space-y-2"> <div className="space-y-2">
<label className="text-sm font-medium">Commit Message</label> <HelpTip label="Becomes the commit's message — shows up in the log panel and the PR's commit history.">
<label className="text-sm font-medium w-fit">
Commit Message
</label>
</HelpTip>
<Textarea <Textarea
placeholder="Describe your changes..." placeholder="Describe your changes..."
value={commitMessage} value={commitMessage}
@@ -216,6 +239,17 @@ export function GitActionsPanel({
</Dialog> </Dialog>
{/* Push Action */} {/* Push Action */}
<HelpTip
label={
canPush
? "git push -u origin <branch>. Uploads your local commits to GitHub — never force, safe to repeat."
: "Nothing to push — no local commits sit ahead of the remote branch yet."
}
>
<span
className="block w-full"
tabIndex={!canPush ? 0 : undefined}
>
<Button <Button
className="w-full justify-start" className="w-full justify-start"
variant={canPush ? "default" : "outline"} variant={canPush ? "default" : "outline"}
@@ -237,9 +271,22 @@ export function GitActionsPanel({
</HelpTip> </HelpTip>
)} )}
</Button> </Button>
</span>
</HelpTip>
{/* Create PR Action */} {/* Create PR Action */}
<Dialog open={showPRDialog} onOpenChange={handlePRDialogOpenChange}> <Dialog open={showPRDialog} onOpenChange={handlePRDialogOpenChange}>
<HelpTip
label={
canCreatePR
? "Opens GitHub's PR creation flow for this branch against main."
: "Already on main with nothing ahead of it — there's no branch content to open a PR for."
}
>
<span
className="block w-full"
tabIndex={!canCreatePR ? 0 : undefined}
>
<DialogTrigger asChild> <DialogTrigger asChild>
<Button <Button
className="w-full justify-start" className="w-full justify-start"
@@ -250,18 +297,24 @@ export function GitActionsPanel({
Create Pull Request Create Pull Request
</Button> </Button>
</DialogTrigger> </DialogTrigger>
</span>
</HelpTip>
<DialogContent className="max-w-lg"> <DialogContent className="max-w-lg">
<DialogHeader> <DialogHeader>
<DialogTitle>Create Pull Request</DialogTitle> <DialogTitle>Create Pull Request</DialogTitle>
</DialogHeader> </DialogHeader>
<div className="space-y-4 py-4"> <div className="space-y-4 py-4">
<div className="flex items-center gap-2 text-sm text-muted-foreground"> <HelpTip label="Source branch on the left merges into the target branch on the right.">
<div className="flex items-center gap-2 text-sm text-muted-foreground w-fit">
<Badge variant="outline">{status?.current_branch}</Badge> <Badge variant="outline">{status?.current_branch}</Badge>
<span></span> <span></span>
<Badge variant="outline">main</Badge> <Badge variant="outline">main</Badge>
</div> </div>
</HelpTip>
<div className="space-y-2"> <div className="space-y-2">
<label className="text-sm font-medium">Title</label> <HelpTip label="Becomes the pull request's title on GitHub.">
<label className="text-sm font-medium w-fit">Title</label>
</HelpTip>
<Input <Input
placeholder="PR title..." placeholder="PR title..."
value={prTitle} value={prTitle}
@@ -269,7 +322,11 @@ export function GitActionsPanel({
/> />
</div> </div>
<div className="space-y-2"> <div className="space-y-2">
<label className="text-sm font-medium">Description</label> <HelpTip label="The PR's body on GitHub — markdown supported, shown to reviewers.">
<label className="text-sm font-medium w-fit">
Description
</label>
</HelpTip>
<Textarea <Textarea
placeholder="Describe your changes..." placeholder="Describe your changes..."
value={prBody} value={prBody}
@@ -300,6 +357,8 @@ export function GitActionsPanel({
open={showMergeDialog} open={showMergeDialog}
onOpenChange={handleMergeDialogOpenChange} onOpenChange={handleMergeDialogOpenChange}
> >
<HelpTip label="Merges an open PR via GitHub's API — squash by default, falls back to merge/rebase if blocked by branch protection.">
<span className="block w-full">
<DialogTrigger asChild> <DialogTrigger asChild>
<Button className="w-full justify-start" variant="outline"> <Button className="w-full justify-start" variant="outline">
{isMerging ? ( {isMerging ? (
@@ -310,13 +369,19 @@ export function GitActionsPanel({
Merge PR Merge PR
</Button> </Button>
</DialogTrigger> </DialogTrigger>
</span>
</HelpTip>
<DialogContent className="max-w-sm"> <DialogContent className="max-w-sm">
<DialogHeader> <DialogHeader>
<DialogTitle>Merge Pull Request</DialogTitle> <DialogTitle>Merge Pull Request</DialogTitle>
</DialogHeader> </DialogHeader>
<div className="space-y-4 py-4"> <div className="space-y-4 py-4">
<div className="space-y-2"> <div className="space-y-2">
<label className="text-sm font-medium">PR Number</label> <HelpTip label="The GitHub PR number to merge, e.g. the 42 in .../pull/42.">
<label className="text-sm font-medium w-fit">
PR Number
</label>
</HelpTip>
<Input <Input
type="number" type="number"
placeholder="e.g. 42" placeholder="e.g. 42"
@@ -351,6 +416,8 @@ export function GitActionsPanel({
</Dialog> </Dialog>
{/* Pull Action */} {/* Pull Action */}
<HelpTip label="Fetches origin, then fast-forwards your branch onto it. Refuses (no merge commit) if history has diverged.">
<span className="block w-full">
<Button <Button
className="w-full justify-start" className="w-full justify-start"
variant="outline" variant="outline"
@@ -364,8 +431,12 @@ export function GitActionsPanel({
)} )}
Pull from Remote Pull from Remote
</Button> </Button>
</span>
</HelpTip>
{/* Fetch Action */} {/* Fetch Action */}
<HelpTip label="Updates remote-tracking refs from origin only — never touches your working directory or local branch.">
<span className="block w-full">
<Button <Button
className="w-full justify-start" className="w-full justify-start"
variant="outline" variant="outline"
@@ -379,9 +450,13 @@ export function GitActionsPanel({
)} )}
Fetch Remote Fetch Remote
</Button> </Button>
</span>
</HelpTip>
{/* Rebase Action — destructive, requires confirmation */} {/* Rebase Action — destructive, requires confirmation */}
<AlertDialog> <AlertDialog>
<HelpTip label="Rewrites this branch's commit history on top of another branch. Requires a force-push afterward — use with care.">
<span className="block w-full">
<AlertDialogTrigger asChild> <AlertDialogTrigger asChild>
<Button <Button
className="w-full justify-start" className="w-full justify-start"
@@ -396,6 +471,8 @@ export function GitActionsPanel({
Rebase onto Remote Rebase onto Remote
</Button> </Button>
</AlertDialogTrigger> </AlertDialogTrigger>
</span>
</HelpTip>
<AlertDialogContent className="border-destructive bg-destructive/5"> <AlertDialogContent className="border-destructive bg-destructive/5">
<AlertDialogHeader> <AlertDialogHeader>
<AlertDialogTitle>Rebase onto target branch?</AlertDialogTitle> <AlertDialogTitle>Rebase onto target branch?</AlertDialogTitle>
@@ -407,7 +484,11 @@ export function GitActionsPanel({
</AlertDialogDescription> </AlertDialogDescription>
</AlertDialogHeader> </AlertDialogHeader>
<div className="px-6 py-2 space-y-2"> <div className="px-6 py-2 space-y-2">
<label className="text-sm font-medium">Target branch</label> <HelpTip label="Branch/ref to replay commits onto, e.g. origin/main. On conflict, rebase auto-aborts — your tree stays untouched.">
<label className="text-sm font-medium w-fit">
Target branch
</label>
</HelpTip>
<Input <Input
placeholder="Remote ref (e.g. origin/HEAD)" placeholder="Remote ref (e.g. origin/HEAD)"
value={rebaseTargetBranch} value={rebaseTargetBranch}
+41 -6
View File
@@ -92,19 +92,27 @@ export function GitBranchPanel({
Branches Branches
</span> </span>
<Dialog open={showCreateDialog} onOpenChange={setShowCreateDialog}> <Dialog open={showCreateDialog} onOpenChange={setShowCreateDialog}>
<HelpTip label="Creates a new branch named {type}/{team}/{task-id}, branched from the task's parent branch (or the project default).">
<span className="inline-block">
<DialogTrigger asChild> <DialogTrigger asChild>
<Button size="sm" variant="outline"> <Button size="sm" variant="outline">
<Plus className="h-3 w-3 mr-1" /> <Plus className="h-3 w-3 mr-1" />
New New
</Button> </Button>
</DialogTrigger> </DialogTrigger>
</span>
</HelpTip>
<DialogContent> <DialogContent>
<DialogHeader> <DialogHeader>
<DialogTitle>Create Task Branch</DialogTitle> <DialogTitle>Create Task Branch</DialogTitle>
</DialogHeader> </DialogHeader>
<div className="space-y-4 py-4"> <div className="space-y-4 py-4">
<div className="space-y-2"> <div className="space-y-2">
<label className="text-sm font-medium">Branch Type</label> <HelpTip label="Sets the branch-name prefix (feature/bug/chore/docs/hotfix) — doesn't otherwise change behavior.">
<label className="text-sm font-medium w-fit">
Branch Type
</label>
</HelpTip>
<Select <Select
value={newBranchType} value={newBranchType}
onValueChange={(v) => setNewBranchType(v as BranchType)} onValueChange={(v) => setNewBranchType(v as BranchType)}
@@ -122,7 +130,11 @@ export function GitBranchPanel({
</Select> </Select>
</div> </div>
<div className="space-y-2"> <div className="space-y-2">
<label className="text-sm font-medium">Task ID</label> <HelpTip label="Identifies the task this branch is for — gets embedded in the branch name (shortened to 8 chars).">
<label className="text-sm font-medium w-fit">
Task ID
</label>
</HelpTip>
<Input <Input
placeholder="Enter task ID..." placeholder="Enter task ID..."
value={taskId} value={taskId}
@@ -156,13 +168,26 @@ export function GitBranchPanel({
<div className="space-y-3"> <div className="space-y-3">
{/* Local Branches */} {/* Local Branches */}
<div> <div>
<h4 className="text-xs font-semibold text-muted-foreground uppercase tracking-wider mb-1"> <HelpTip label="Branches that exist in your local clone's .git — checking one out is instant, no fetch needed.">
<h4 className="text-xs font-semibold text-muted-foreground uppercase tracking-wider mb-1 w-fit">
Local ({localBranches.length}) Local ({localBranches.length})
</h4> </h4>
</HelpTip>
<div className="space-y-0.5"> <div className="space-y-0.5">
{localBranches.map((branch) => ( {localBranches.map((branch) => (
<Button <HelpTip
key={branch.name} key={branch.name}
label={
branch.is_current
? "This is the branch you're already on."
: "Switches your workspace to this branch (git checkout). Non-conflicting uncommitted changes carry over."
}
>
<span
className="block"
tabIndex={branch.is_current ? 0 : undefined}
>
<Button
onClick={() => onClick={() =>
!branch.is_current && onCheckout(branch.name) !branch.is_current && onCheckout(branch.name)
} }
@@ -191,6 +216,8 @@ export function GitBranchPanel({
</HelpTip> </HelpTip>
)} )}
</Button> </Button>
</span>
</HelpTip>
))} ))}
</div> </div>
</div> </div>
@@ -198,14 +225,20 @@ export function GitBranchPanel({
{/* Remote Branches */} {/* Remote Branches */}
{remoteBranches.length > 0 && ( {remoteBranches.length > 0 && (
<div> <div>
<h4 className="text-xs font-semibold text-muted-foreground uppercase tracking-wider mb-1 flex items-center gap-1"> <HelpTip label="Branches on origin without a local counterpart yet — checking one out creates a local tracking branch.">
<h4 className="text-xs font-semibold text-muted-foreground uppercase tracking-wider mb-1 flex items-center gap-1 w-fit">
<Cloud className="h-3 w-3" /> <Cloud className="h-3 w-3" />
Remote ({remoteBranches.length}) Remote ({remoteBranches.length})
</h4> </h4>
</HelpTip>
<div className="space-y-0.5"> <div className="space-y-0.5">
{remoteBranches.map((branch) => ( {remoteBranches.map((branch) => (
<Button <HelpTip
key={branch.name} key={branch.name}
label="Creates a local branch tracking this remote ref and switches to it."
>
<span className="block">
<Button
onClick={() => onCheckout(branch.name)} onClick={() => onCheckout(branch.name)}
disabled={isCheckingOut} disabled={isCheckingOut}
variant="ghost" variant="ghost"
@@ -215,6 +248,8 @@ export function GitBranchPanel({
{branch.name} {branch.name}
</span> </span>
</Button> </Button>
</span>
</HelpTip>
))} ))}
</div> </div>
</div> </div>
+5
View File
@@ -18,6 +18,7 @@ import { GitDiffViewer } from "./git-diff-viewer";
import { GitActionsPanel } from "./git-actions-panel"; import { GitActionsPanel } from "./git-actions-panel";
import { GitBranch, FolderGit2 } from "lucide-react"; import { GitBranch, FolderGit2 } from "lucide-react";
import { useGitBrowser } from "@/hooks/use-git-browser"; import { useGitBrowser } from "@/hooks/use-git-browser";
import { HelpTip } from "@/components/ui/help-tip";
function GitBrowserContent() { function GitBrowserContent() {
const { const {
@@ -80,6 +81,8 @@ function GitBrowserContent() {
</div> </div>
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
{/* Project Selector */} {/* Project Selector */}
<HelpTip label="Switches every panel below (status, branches, log, diff) to this project's clone on disk.">
<span className="w-full sm:w-64">
<Select value={projectSlug} onValueChange={handleProjectChange}> <Select value={projectSlug} onValueChange={handleProjectChange}>
<SelectTrigger className="w-full sm:w-64"> <SelectTrigger className="w-full sm:w-64">
<FolderGit2 className="h-4 w-4 mr-2" /> <FolderGit2 className="h-4 w-4 mr-2" />
@@ -99,6 +102,8 @@ function GitBrowserContent() {
)} )}
</SelectContent> </SelectContent>
</Select> </Select>
</span>
</HelpTip>
</div> </div>
</div> </div>
+11 -2
View File
@@ -104,21 +104,30 @@ export function GitDiffViewer({
<Card> <Card>
<CardHeader className="pb-2"> <CardHeader className="pb-2">
<div className="flex items-center justify-between gap-2"> <div className="flex items-center justify-between gap-2">
<CardTitle className="text-sm flex items-center gap-2"> <HelpTip label="Green = added lines, red = removed, blue = hunk header (@@ ... @@ shows the surrounding line range).">
<CardTitle className="text-sm flex items-center gap-2 w-fit">
<FileCode className="h-4 w-4" /> <FileCode className="h-4 w-4" />
Changes Changes
</CardTitle> </CardTitle>
</HelpTip>
<HelpTip
label={
wrap
? "Long lines wrap to fit the panel — click to scroll horizontally instead."
: "Long lines overflow with horizontal scroll — click to wrap them instead."
}
>
<Button <Button
variant={wrap ? "secondary" : "ghost"} variant={wrap ? "secondary" : "ghost"}
size="sm" size="sm"
className="h-7 px-2 text-xs" className="h-7 px-2 text-xs"
aria-pressed={wrap} aria-pressed={wrap}
onClick={() => setWrap((w) => !w)} onClick={() => setWrap((w) => !w)}
title="Toggle line wrap"
> >
<WrapText className="h-3.5 w-3.5 mr-1" /> <WrapText className="h-3.5 w-3.5 mr-1" />
Wrap Wrap
</Button> </Button>
</HelpTip>
</div> </div>
</CardHeader> </CardHeader>
<CardContent className="p-0"> <CardContent className="p-0">
+10 -2
View File
@@ -62,17 +62,22 @@ export function GitLogPanel({
<CardTitle className="text-sm flex items-center gap-2"> <CardTitle className="text-sm flex items-center gap-2">
<GitCommit className="h-4 w-4" /> <GitCommit className="h-4 w-4" />
Commit History Commit History
<HelpTip label="Commits reachable from this branch, newest first.">
<Badge variant="secondary" className="ml-auto text-xs"> <Badge variant="secondary" className="ml-auto text-xs">
{log.branch} {log.branch}
</Badge> </Badge>
</HelpTip>
</CardTitle> </CardTitle>
</CardHeader> </CardHeader>
<CardContent className="p-0"> <CardContent className="p-0">
<ScrollArea className="h-80"> <ScrollArea className="h-80">
<div className="p-4 space-y-0"> <div className="p-4 space-y-0">
{log.commits.map((commit, index) => ( {log.commits.map((commit, index) => (
<Button <HelpTip
key={commit.hash} key={commit.hash}
label={onSelectCommit ? "Select this commit" : ""}
>
<Button
onClick={() => onSelectCommit?.(commit)} onClick={() => onSelectCommit?.(commit)}
variant="ghost" variant="ghost"
className={cn( className={cn(
@@ -120,16 +125,19 @@ export function GitLogPanel({
<User className="h-3 w-3" /> <User className="h-3 w-3" />
{commit.author} {commit.author}
</span> </span>
<span className="flex items-center gap-1"> <HelpTip label={new Date(commit.date).toLocaleString()}>
<span className="flex items-center gap-1 w-fit">
<Calendar className="h-3 w-3" /> <Calendar className="h-3 w-3" />
{formatDistanceToNow(new Date(commit.date), { {formatDistanceToNow(new Date(commit.date), {
addSuffix: true, addSuffix: true,
})} })}
</span> </span>
</HelpTip>
</div> </div>
</div> </div>
</div> </div>
</Button> </Button>
</HelpTip>
))} ))}
</div> </div>
</ScrollArea> </ScrollArea>
@@ -70,20 +70,24 @@ export function GitStatusPanel({ status, isLoading }: GitStatusPanelProps) {
Repository Status Repository Status
</span> </span>
{!hasChanges && ( {!hasChanges && (
<HelpTip label="No staged, modified, or untracked files — the working tree matches HEAD.">
<Badge variant="outline" className="text-green-600"> <Badge variant="outline" className="text-green-600">
<CheckCircle className="h-3 w-3 mr-1" /> <CheckCircle className="h-3 w-3 mr-1" />
Clean Clean
</Badge> </Badge>
</HelpTip>
)} )}
</CardTitle> </CardTitle>
</CardHeader> </CardHeader>
<CardContent className="space-y-4"> <CardContent className="space-y-4">
{/* Branch Info */} {/* Branch Info */}
<div className="flex items-center gap-4 text-sm"> <div className="flex items-center gap-4 text-sm">
<div className="flex items-center gap-2"> <HelpTip label="The branch currently checked out in this project's workspace.">
<div className="flex items-center gap-2 w-fit">
<GitBranch className="h-4 w-4 text-muted-foreground" /> <GitBranch className="h-4 w-4 text-muted-foreground" />
<span className="font-medium">{status.current_branch}</span> <span className="font-medium">{status.current_branch}</span>
</div> </div>
</HelpTip>
{(status.ahead > 0 || status.behind > 0) && ( {(status.ahead > 0 || status.behind > 0) && (
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
{status.ahead > 0 && ( {status.ahead > 0 && (
@@ -16,6 +16,7 @@ import {
TooltipContent, TooltipContent,
TooltipTrigger, TooltipTrigger,
} from "@/components/ui/tooltip"; } from "@/components/ui/tooltip";
import { HelpTip } from "@/components/ui/help-tip";
import { ChevronLeft, ChevronRight } from "lucide-react"; import { ChevronLeft, ChevronRight } from "lucide-react";
import { toast } from "sonner"; import { toast } from "sonner";
import { import {
@@ -391,6 +392,13 @@ export function KanbanBoard({
{/* Mobile: single-column view with prev/next navigator (hidden on sm+) */} {/* Mobile: single-column view with prev/next navigator (hidden on sm+) */}
<div className="sm:hidden flex flex-1 min-h-0 flex-col"> <div className="sm:hidden flex flex-1 min-h-0 flex-col">
<div className="flex items-center gap-2 mb-4 shrink-0"> <div className="flex items-center gap-2 mb-4 shrink-0">
{/* Buttons wrapped in a span, not tipped directly: disabled sets
pointer-events:none on the Button and would swallow hover. */}
<HelpTip label="Swipe to the previous lifecycle stage — each column is one task status">
<span
className="shrink-0"
tabIndex={activeColumnIndex === 0 ? undefined : 0}
>
<Button <Button
variant="outline" variant="outline"
size="icon" size="icon"
@@ -401,24 +409,37 @@ export function KanbanBoard({
> >
<ChevronLeft className="h-5 w-5" /> <ChevronLeft className="h-5 w-5" />
</Button> </Button>
</span>
</HelpTip>
<p className="flex-1 text-center text-sm font-semibold"> <p className="flex-1 text-center text-sm font-semibold">
<span className="text-muted-foreground font-normal text-xs mr-1"> <span className="text-muted-foreground font-normal text-xs mr-1">
{activeColumnIndex + 1}/{columns.length} {activeColumnIndex + 1}/{columns.length}
</span> </span>
{columns[activeColumnIndex]?.title} {columns[activeColumnIndex]?.title}
</p> </p>
<HelpTip label="Swipe to the next lifecycle stage — each column is one task status">
<span
className="shrink-0"
tabIndex={
activeColumnIndex === columns.length - 1 ? undefined : 0
}
>
<Button <Button
variant="outline" variant="outline"
size="icon" size="icon"
className="min-h-11 min-w-11 shrink-0" className="min-h-11 min-w-11 shrink-0"
onClick={() => onClick={() =>
setActiveColumnIndex((i) => Math.min(columns.length - 1, i + 1)) setActiveColumnIndex((i) =>
Math.min(columns.length - 1, i + 1),
)
} }
disabled={activeColumnIndex === columns.length - 1} disabled={activeColumnIndex === columns.length - 1}
aria-label="Next column" aria-label="Next column"
> >
<ChevronRight className="h-5 w-5" /> <ChevronRight className="h-5 w-5" />
</Button> </Button>
</span>
</HelpTip>
</div> </div>
{columns[activeColumnIndex] && ( {columns[activeColumnIndex] && (
<KanbanColumn <KanbanColumn
@@ -17,6 +17,7 @@ import {
TooltipProvider, TooltipProvider,
TooltipTrigger, TooltipTrigger,
} from "@/components/ui/tooltip"; } from "@/components/ui/tooltip";
import { HelpTip } from "@/components/ui/help-tip";
import { PriorityIndicator } from "../shared/priority-indicator"; import { PriorityIndicator } from "../shared/priority-indicator";
import { BlockedBadge } from "../shared/blocked-badge"; import { BlockedBadge } from "../shared/blocked-badge";
import { AssigneeAvatar } from "../shared/assignee-avatar"; import { AssigneeAvatar } from "../shared/assignee-avatar";
@@ -109,12 +110,13 @@ export function KanbanCard({
<div className="flex-1 min-w-0 overflow-hidden"> <div className="flex-1 min-w-0 overflow-hidden">
<Link href={"/tasks/" + task.id} className="block" prefetch={false}> <Link href={"/tasks/" + task.id} className="block" prefetch={false}>
<p className="font-medium text-sm line-clamp-2 hover:underline break-words"> <p className="font-medium text-sm line-clamp-2 hover:underline break-words">
<span <HelpTip
className="font-mono text-muted-foreground" label={`Full ID ${task.id} — the 8-char prefix is what commits and branch names for this task use`}
title={task.id}
> >
<span className="font-mono text-muted-foreground">
#{task.id.slice(0, 8)} #{task.id.slice(0, 8)}
</span>{" "} </span>
</HelpTip>{" "}
{task.title} {task.title}
</p> </p>
</Link> </Link>
@@ -144,9 +146,11 @@ export function KanbanCard({
<div className="flex items-center justify-between mt-2"> <div className="flex items-center justify-between mt-2">
<div className="flex items-center gap-1 flex-wrap"> <div className="flex items-center gap-1 flex-wrap">
<HelpTip label="Delivery cell that owns this task — only its agents can claim it">
<Badge variant="outline" className="text-xs"> <Badge variant="outline" className="text-xs">
{task.team.replace(/_/g, " ")} {task.team.replace(/_/g, " ")}
</Badge> </Badge>
</HelpTip>
<TaskTypeBadge type={task.task_type} showLabel={false} /> <TaskTypeBadge type={task.task_type} showLabel={false} />
{task.sequence != null && ( {task.sequence != null && (
<Tooltip> <Tooltip>
@@ -8,6 +8,8 @@ import {
TooltipContent, TooltipContent,
TooltipTrigger, TooltipTrigger,
} from "@/components/ui/tooltip"; } from "@/components/ui/tooltip";
import { HelpTip } from "@/components/ui/help-tip";
import { taskStatusDescription } from "@/components/tasks/task-status-badge";
import { KanbanCard } from "./kanban-card"; import { KanbanCard } from "./kanban-card";
import { useDroppable } from "@dnd-kit/core"; import { useDroppable } from "@dnd-kit/core";
import { cn } from "@/lib/utils"; import { cn } from "@/lib/utils";
@@ -55,9 +57,19 @@ export function KanbanColumn({
)} )}
> >
<div className="flex items-center justify-between mb-3"> <div className="flex items-center justify-between mb-3">
<h3 className="font-semibold text-sm text-gray-800 dark:text-gray-100"> {/* Reuses the canonical per-status copy (task-status-badge.tsx) so a
column header explains what a drag-drop here actually does. */}
<HelpTip
label={
taskStatusDescription(status)
? `Dropping a card here fires this transition: ${taskStatusDescription(status)}`
: null
}
>
<h3 className="font-semibold text-sm text-gray-800 dark:text-gray-100 w-fit">
{title} {title}
</h3> </h3>
</HelpTip>
<Tooltip> <Tooltip>
<TooltipTrigger asChild> <TooltipTrigger asChild>
<Badge <Badge
@@ -267,6 +267,7 @@ export function BatchReviewCard({
)} )}
<div className="flex flex-wrap gap-2 pt-1"> <div className="flex flex-wrap gap-2 pt-1">
<HelpTip label="Dismisses this review and returns to chat — nothing is launched, the draft batch is kept">
<Button <Button
variant="outline" variant="outline"
size="sm" size="sm"
@@ -276,6 +277,7 @@ export function BatchReviewCard({
<MessageCircle className="mr-1.5 h-3.5 w-3.5" /> <MessageCircle className="mr-1.5 h-3.5 w-3.5" />
Keep chatting Keep chatting
</Button> </Button>
</HelpTip>
{/* Board review & Start → the Board reviews the whole batch first */} {/* Board review & Start → the Board reviews the whole batch first */}
<HelpTip label="Routes the whole MegaTask through the Product Owner and Head of Marketing for review before any work starts."> <HelpTip label="Routes the whole MegaTask through the Product Owner and Head of Marketing for review before any work starts.">
<Button <Button
@@ -78,6 +78,7 @@ export function BoardReviewSentCard({
</CardContent> </CardContent>
<CardFooter className="gap-2 pt-0"> <CardFooter className="gap-2 pt-0">
<HelpTip label="Opens the umbrella task in a new tab — use Approve & Start there once the board finishes">
<Button variant="outline" size="sm" asChild className="flex-1"> <Button variant="outline" size="sm" asChild className="flex-1">
<Link <Link
prefetch={false} prefetch={false}
@@ -89,10 +90,13 @@ export function BoardReviewSentCard({
View umbrella task View umbrella task
</Link> </Link>
</Button> </Button>
</HelpTip>
<HelpTip label="Ends this session and resets to a fresh intake chat">
<Button size="sm" className="flex-1" onClick={onStartAnother}> <Button size="sm" className="flex-1" onClick={onStartAnother}>
<RefreshCw className="mr-1.5 h-3.5 w-3.5" /> <RefreshCw className="mr-1.5 h-3.5 w-3.5" />
Start another Start another
</Button> </Button>
</HelpTip>
</CardFooter> </CardFooter>
</Card> </Card>
); );
@@ -8,6 +8,7 @@ import {
TooltipContent, TooltipContent,
TooltipTrigger, TooltipTrigger,
} from "@/components/ui/tooltip"; } from "@/components/ui/tooltip";
import { HelpTip } from "@/components/ui/help-tip";
import { Textarea } from "@/components/ui/textarea"; import { Textarea } from "@/components/ui/textarea";
interface ChatComposerProps { interface ChatComposerProps {
@@ -57,6 +58,7 @@ export function ChatComposer({
className="flex items-end gap-2 border-t bg-background px-4 py-3" className="flex items-end gap-2 border-t bg-background px-4 py-3"
style={{ paddingBottom: "max(0.75rem, env(safe-area-inset-bottom))" }} style={{ paddingBottom: "max(0.75rem, env(safe-area-inset-bottom))" }}
> >
<HelpTip label="Enter sends; Shift+Enter inserts a newline">
<Textarea <Textarea
ref={textareaRef} ref={textareaRef}
value={value} value={value}
@@ -67,6 +69,7 @@ export function ChatComposer({
rows={2} rows={2}
className="flex-1 resize-none text-sm" className="flex-1 resize-none text-sm"
/> />
</HelpTip>
<Tooltip> <Tooltip>
<TooltipTrigger asChild> <TooltipTrigger asChild>
<Button <Button
@@ -7,6 +7,7 @@ import ReactMarkdown from "react-markdown";
import remarkGfm from "remark-gfm"; import remarkGfm from "remark-gfm";
import { cn } from "@/lib/utils"; import { cn } from "@/lib/utils";
import { CopyButton } from "@/components/ui/copy-button"; import { CopyButton } from "@/components/ui/copy-button";
import { HelpTip } from "@/components/ui/help-tip";
import type { ChatMessage, StartRoute } from "@/hooks/use-prompter"; import type { ChatMessage, StartRoute } from "@/hooks/use-prompter";
import { DraftProposalCard } from "./draft-proposal-card"; import { DraftProposalCard } from "./draft-proposal-card";
@@ -110,7 +111,11 @@ export function ChatMessages({
return ( return (
<div key={msg.id} className="flex justify-start"> <div key={msg.id} className="flex justify-start">
<div className="group relative flex max-w-[70%] items-start gap-2 rounded-2xl rounded-tl-sm border border-destructive/30 bg-destructive/10 px-4 py-3 text-sm text-destructive"> <div className="group relative flex max-w-[70%] items-start gap-2 rounded-2xl rounded-tl-sm border border-destructive/30 bg-destructive/10 px-4 py-3 text-sm text-destructive">
<AlertTriangle className="mt-0.5 h-4 w-4 shrink-0" /> <HelpTip label="A request to the agent failed — this message did not come from it">
<span className="mt-0.5 shrink-0" tabIndex={0}>
<AlertTriangle className="h-4 w-4" />
</span>
</HelpTip>
<span>{msg.content}</span> <span>{msg.content}</span>
<CopyButton <CopyButton
value={msg.content} value={msg.content}
@@ -88,17 +88,23 @@ export function DraftProposalCard({
</CardTitle> </CardTitle>
<div className="flex flex-wrap items-center gap-1 shrink-0"> <div className="flex flex-wrap items-center gap-1 shrink-0">
{draft.team && ( {draft.team && (
<HelpTip label="Delivery cell this task will be routed to">
<Badge variant="secondary" className="text-xs"> <Badge variant="secondary" className="text-xs">
{draft.team} {draft.team}
</Badge> </Badge>
</HelpTip>
)} )}
<HelpTip label="How urgently agents work this once created — higher priority is claimed first">
<Badge variant="outline" className="text-xs"> <Badge variant="outline" className="text-xs">
{priorityLabel} {priorityLabel}
</Badge> </Badge>
</HelpTip>
{draft.task_type && ( {draft.task_type && (
<HelpTip label="Kind of work this is — affects which role picks it up">
<Badge variant="outline" className="text-xs"> <Badge variant="outline" className="text-xs">
{draft.task_type} {draft.task_type}
</Badge> </Badge>
</HelpTip>
)} )}
<CopyButton value={draftToText(draft)} className="ml-0.5" /> <CopyButton value={draftToText(draft)} className="ml-0.5" />
</div> </div>
@@ -161,6 +167,7 @@ export function DraftProposalCard({
</CardContent> </CardContent>
<CardFooter className="flex-wrap gap-2 pt-0"> <CardFooter className="flex-wrap gap-2 pt-0">
<HelpTip label="Dismisses this card and returns to chat — nothing is launched, the draft is kept">
<Button <Button
variant="outline" variant="outline"
size="sm" size="sm"
@@ -170,6 +177,7 @@ export function DraftProposalCard({
<MessageCircle className="mr-1.5 h-3.5 w-3.5" /> <MessageCircle className="mr-1.5 h-3.5 w-3.5" />
Keep chatting Keep chatting
</Button> </Button>
</HelpTip>
{/* Board review & Start → PENDING, assigned to PO + HoM for review */} {/* Board review & Start → PENDING, assigned to PO + HoM for review */}
<HelpTip label="Routes this task through the Product Owner and Head of Marketing for review before any work starts."> <HelpTip label="Routes this task through the Product Owner and Head of Marketing for review before any work starts.">
<Button <Button
@@ -139,9 +139,11 @@ export function IntakeForm({
onValueChange={onProjectId} onValueChange={onProjectId}
disabled={isPreparing} disabled={isPreparing}
> >
<HelpTip label="Only this project's repo is cloned and read by the agent">
<SelectTrigger> <SelectTrigger>
<SelectValue placeholder="Select a project…" /> <SelectValue placeholder="Select a project…" />
</SelectTrigger> </SelectTrigger>
</HelpTip>
<SelectContent> <SelectContent>
{projects.map((p) => ( {projects.map((p) => (
<SelectItem key={p.id} value={p.id}> <SelectItem key={p.id} value={p.id}>
@@ -201,9 +203,11 @@ export function IntakeForm({
onValueChange={onProductId} onValueChange={onProductId}
disabled={isPreparing} disabled={isPreparing}
> >
<HelpTip label="Every cell repo mapped to this product is cloned and read by the agent">
<SelectTrigger> <SelectTrigger>
<SelectValue placeholder="Select a product…" /> <SelectValue placeholder="Select a product…" />
</SelectTrigger> </SelectTrigger>
</HelpTip>
<SelectContent> <SelectContent>
{products.map((p) => ( {products.map((p) => (
<SelectItem key={p.id} value={p.id}> <SelectItem key={p.id} value={p.id}>
@@ -229,6 +233,7 @@ export function IntakeForm({
What do you want to build?{" "} What do you want to build?{" "}
<span className="text-destructive">*</span> <span className="text-destructive">*</span>
</Label> </Label>
<HelpTip label="Kicks off the interview — you can refine details in chat afterward">
<Textarea <Textarea
id="intake-initial-message" id="intake-initial-message"
value={initialMessage} value={initialMessage}
@@ -237,6 +242,7 @@ export function IntakeForm({
rows={4} rows={4}
disabled={isPreparing} disabled={isPreparing}
/> />
</HelpTip>
</div> </div>
{/* Button's own disabled:pointer-events-none would swallow hover, so {/* Button's own disabled:pointer-events-none would swallow hover, so
@@ -41,9 +41,11 @@ export function SuccessCard({
<CardContent className="pb-3 space-y-2"> <CardContent className="pb-3 space-y-2">
<p className="text-sm font-medium">{taskTitle}</p> <p className="text-sm font-medium">{taskTitle}</p>
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<HelpTip label="Delivery cell this task was routed to">
<Badge variant="secondary" className="text-xs"> <Badge variant="secondary" className="text-xs">
{team.replace("_", " ")} {team.replace("_", " ")}
</Badge> </Badge>
</HelpTip>
<HelpTip label={`Full task ID: ${taskId}`}> <HelpTip label={`Full task ID: ${taskId}`}>
<span className="text-xs text-muted-foreground"> <span className="text-xs text-muted-foreground">
ID: {taskId.slice(0, 8)} ID: {taskId.slice(0, 8)}
@@ -53,6 +55,7 @@ export function SuccessCard({
</CardContent> </CardContent>
<CardFooter className="gap-2 pt-0"> <CardFooter className="gap-2 pt-0">
<HelpTip label="Opens the new task's detail page in a new tab">
<Button variant="outline" size="sm" asChild className="flex-1"> <Button variant="outline" size="sm" asChild className="flex-1">
<Link <Link
prefetch={false} prefetch={false}
@@ -64,10 +67,13 @@ export function SuccessCard({
View Task View Task
</Link> </Link>
</Button> </Button>
</HelpTip>
<HelpTip label="Ends this session and resets to a fresh intake chat">
<Button size="sm" className="flex-1" onClick={onStartAnother}> <Button size="sm" className="flex-1" onClick={onStartAnother}>
<RefreshCw className="mr-1.5 h-3.5 w-3.5" /> <RefreshCw className="mr-1.5 h-3.5 w-3.5" />
Start Another Start Another
</Button> </Button>
</HelpTip>
</CardFooter> </CardFooter>
</Card> </Card>
); );
@@ -10,6 +10,7 @@ import {
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 { Plus, X, GripVertical } from "lucide-react"; import { Plus, X, GripVertical } from "lucide-react";
import { HelpTip } from "@/components/ui/help-tip";
interface AcceptanceCriteriaEditorProps { interface AcceptanceCriteriaEditorProps {
criteria: string[]; criteria: string[];
@@ -53,12 +54,16 @@ export function AcceptanceCriteriaEditor({
return ( return (
<div className="space-y-3"> <div className="space-y-3">
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
<HelpTip label="QA and reviewers check each of these directly against the shipped work to decide pass/fail.">
<Label> <Label>
Acceptance Criteria <span className="text-destructive">*</span> Acceptance Criteria <span className="text-destructive">*</span>
</Label> </Label>
</HelpTip>
<HelpTip label="How many criteria are defined so far — at least one is required to submit.">
<span className="text-xs text-muted-foreground"> <span className="text-xs text-muted-foreground">
{criteria.length} item{criteria.length !== 1 ? "s" : ""} {criteria.length} item{criteria.length !== 1 ? "s" : ""}
</span> </span>
</HelpTip>
</div> </div>
{/* Existing criteria list */} {/* Existing criteria list */}
@@ -70,11 +75,13 @@ export function AcceptanceCriteriaEditor({
<span className="text-sm text-muted-foreground w-6"> <span className="text-sm text-muted-foreground w-6">
{index + 1}. {index + 1}.
</span> </span>
<HelpTip label="Edit this criterion's text directly — changes save as you type.">
<Input <Input
value={criterion} value={criterion}
onChange={(e) => handleUpdate(index, e.target.value)} onChange={(e) => handleUpdate(index, e.target.value)}
className="flex-1 h-8" className="flex-1 h-8"
/> />
</HelpTip>
<Tooltip> <Tooltip>
<TooltipTrigger asChild> <TooltipTrigger asChild>
<Button <Button
@@ -96,6 +103,7 @@ export function AcceptanceCriteriaEditor({
{/* Add new criterion */} {/* Add new criterion */}
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<HelpTip label="A specific, testable condition — Enter or Add appends it to the list above.">
<Input <Input
value={newCriterion} value={newCriterion}
onChange={(e) => setNewCriterion(e.target.value)} onChange={(e) => setNewCriterion(e.target.value)}
@@ -103,6 +111,12 @@ export function AcceptanceCriteriaEditor({
placeholder="Enter acceptance criterion and press Enter..." placeholder="Enter acceptance criterion and press Enter..."
className="flex-1" className="flex-1"
/> />
</HelpTip>
<HelpTip label="Appends the text on the left as a new criterion; disabled until you type something.">
<span
className="inline-block"
tabIndex={!newCriterion.trim() ? 0 : undefined}
>
<Button <Button
type="button" type="button"
variant="outline" variant="outline"
@@ -112,6 +126,8 @@ export function AcceptanceCriteriaEditor({
<Plus className="h-4 w-4 mr-1" /> <Plus className="h-4 w-4 mr-1" />
Add Add
</Button> </Button>
</span>
</HelpTip>
</div> </div>
{/* Helper text */} {/* Helper text */}
@@ -74,6 +74,7 @@ export function ApproveAndStartButton({ task }: ApproveAndStartButtonProps) {
return ( return (
<> <>
<HelpTip label="Hands this task to the Main PM and unblocks any BACKLOG child tasks (CEO-only).">
<Button <Button
size="sm" size="sm"
className="bg-green-600 hover:bg-green-700" className="bg-green-600 hover:bg-green-700"
@@ -82,6 +83,7 @@ export function ApproveAndStartButton({ task }: ApproveAndStartButtonProps) {
<Rocket className="h-4 w-4 mr-1" /> <Rocket className="h-4 w-4 mr-1" />
Approve &amp; Start Approve &amp; Start
</Button> </Button>
</HelpTip>
<Dialog <Dialog
open={open} open={open}
@@ -97,7 +99,9 @@ export function ApproveAndStartButton({ task }: ApproveAndStartButtonProps) {
</DialogHeader> </DialogHeader>
<div className="space-y-2"> <div className="space-y-2">
<HelpTip label="Fetched live only while this dialog is open — read it before approving.">
<Label>Board review (Product Owner &amp; Head of Marketing)</Label> <Label>Board review (Product Owner &amp; Head of Marketing)</Label>
</HelpTip>
{boardLoading ? ( {boardLoading ? (
<p className="text-xs text-muted-foreground"> <p className="text-xs text-muted-foreground">
Loading board review Loading board review
@@ -140,9 +144,16 @@ export function ApproveAndStartButton({ task }: ApproveAndStartButtonProps) {
</div> </div>
<DialogFooter> <DialogFooter>
<HelpTip label="Closes without approving; the note above is discarded.">
<Button variant="outline" onClick={closeDialog}> <Button variant="outline" onClick={closeDialog}>
Cancel Cancel
</Button> </Button>
</HelpTip>
<HelpTip label="Submits the approval note and hands the task to the Main PM.">
<span
className="inline-block"
tabIndex={approveAndStartMutation.isPending ? 0 : undefined}
>
<Button <Button
onClick={handleConfirm} onClick={handleConfirm}
disabled={approveAndStartMutation.isPending} disabled={approveAndStartMutation.isPending}
@@ -152,6 +163,8 @@ export function ApproveAndStartButton({ task }: ApproveAndStartButtonProps) {
? "Starting..." ? "Starting..."
: "Approve & Start"} : "Approve & Start"}
</Button> </Button>
</span>
</HelpTip>
</DialogFooter> </DialogFooter>
</DialogContent> </DialogContent>
</Dialog> </Dialog>
@@ -233,9 +233,11 @@ export function CreateTaskDialog() {
<form onSubmit={handleSubmit} className="space-y-6 mt-4"> <form onSubmit={handleSubmit} className="space-y-6 mt-4">
{/* Title */} {/* Title */}
<div className="space-y-2"> <div className="space-y-2">
<HelpTip label="5-200 characters. Shown in the task list, task detail, and used as the default PR title.">
<Label htmlFor="title"> <Label htmlFor="title">
Title <span className="text-destructive">*</span> Title <span className="text-destructive">*</span>
</Label> </Label>
</HelpTip>
<Input <Input
id="title" id="title"
value={title} value={title}
@@ -262,9 +264,11 @@ export function CreateTaskDialog() {
{/* Team, Priority, Complexity, Status, Nature */} {/* Team, Priority, Complexity, Status, Nature */}
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-5 gap-4"> <div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-5 gap-4">
<div className="space-y-2"> <div className="space-y-2">
<HelpTip label="Which cell owns this task — governs claim eligibility and the branch name (feature/{team}/...).">
<Label> <Label>
Team <span className="text-destructive">*</span> Team <span className="text-destructive">*</span>
</Label> </Label>
</HelpTip>
<Select value={team} onValueChange={(v) => setTeam(v as Team)}> <Select value={team} onValueChange={(v) => setTeam(v as Team)}>
<SelectTrigger className="w-full"> <SelectTrigger className="w-full">
<SelectValue /> <SelectValue />
@@ -279,7 +283,9 @@ export function CreateTaskDialog() {
</Select> </Select>
</div> </div>
<div className="space-y-2"> <div className="space-y-2">
<HelpTip label="Pending is claimable immediately; Backlog waits for PM setup (dependencies/session) before it can be activated.">
<Label>Status</Label> <Label>Status</Label>
</HelpTip>
<Select <Select
value={status} value={status}
onValueChange={(v) => setStatus(v as TaskStatus)} onValueChange={(v) => setStatus(v as TaskStatus)}
@@ -297,7 +303,9 @@ export function CreateTaskDialog() {
</Select> </Select>
</div> </div>
<div className="space-y-2"> <div className="space-y-2">
<HelpTip label="Orders claim/dispatch queues — P0 tasks surface first. Doesn't force who claims it or block lower priorities.">
<Label>Priority</Label> <Label>Priority</Label>
</HelpTip>
<Select <Select
value={String(priority)} value={String(priority)}
onValueChange={(v) => setPriority(parseInt(v, 10))} onValueChange={(v) => setPriority(parseInt(v, 10))}
@@ -315,7 +323,9 @@ export function CreateTaskDialog() {
</Select> </Select>
</div> </div>
<div className="space-y-2"> <div className="space-y-2">
<HelpTip label="Medium/High auto-routes a code task to the cell or Main PM to break down; a bare High dev task with no subtasks self-blocks.">
<Label>Complexity</Label> <Label>Complexity</Label>
</HelpTip>
<Select <Select
value={complexity} value={complexity}
onValueChange={(v) => setComplexity(v as Complexity)} onValueChange={(v) => setComplexity(v as Complexity)}
@@ -333,7 +343,9 @@ export function CreateTaskDialog() {
</Select> </Select>
</div> </div>
<div className="space-y-2"> <div className="space-y-2">
<HelpTip label="Filtering only: Non-Technical root tasks awaiting PM review surface in the Board's strategic queue.">
<Label>Nature</Label> <Label>Nature</Label>
</HelpTip>
<Select <Select
value={nature} value={nature}
onValueChange={(v) => setNature(v as TaskNature)} onValueChange={(v) => setNature(v as TaskNature)}
@@ -373,7 +385,9 @@ export function CreateTaskDialog() {
type="button" type="button"
className="w-full justify-between" className="w-full justify-between"
> >
<HelpTip label="Parent task, agent assignment, task type, and repo/product routing.">
<span>Advanced Options</span> <span>Advanced Options</span>
</HelpTip>
{advancedOpen ? ( {advancedOpen ? (
<ChevronDown className="h-4 w-4" /> <ChevronDown className="h-4 w-4" />
) : ( ) : (
@@ -384,7 +398,9 @@ export function CreateTaskDialog() {
<CollapsibleContent className="space-y-4 pt-4"> <CollapsibleContent className="space-y-4 pt-4">
{/* Parent Task */} {/* Parent Task */}
<div className="space-y-2"> <div className="space-y-2">
<HelpTip label="Nests this task under another — its claim order is then gated by sequence relative to siblings under the same parent.">
<Label>Parent Task (for subtasks)</Label> <Label>Parent Task (for subtasks)</Label>
</HelpTip>
<TaskSelector <TaskSelector
value={parentTaskId} value={parentTaskId}
onChange={setParentTaskId} onChange={setParentTaskId}
@@ -398,7 +414,9 @@ export function CreateTaskDialog() {
{/* Assign To */} {/* Assign To */}
<div className="space-y-2"> <div className="space-y-2">
<HelpTip label="Pin a specific agent; leave unassigned to let the orchestrator route by role, team, and availability.">
<Label>Assign To</Label> <Label>Assign To</Label>
</HelpTip>
<AgentSelector <AgentSelector
value={assignedTo} value={assignedTo}
onChange={setAssignedTo} onChange={setAssignedTo}
@@ -415,9 +433,11 @@ export function CreateTaskDialog() {
<div className="space-y-4 pt-4 border-t"> <div className="space-y-4 pt-4 border-t">
<div className="flex items-center gap-2 mb-2"> <div className="flex items-center gap-2 mb-2">
<GitBranch className="h-4 w-4 text-muted-foreground" /> <GitBranch className="h-4 w-4 text-muted-foreground" />
<HelpTip label="Every task type follows the same branch → commits → PR workflow; these fields just route it.">
<span className="font-medium text-sm"> <span className="font-medium text-sm">
Git & Work Configuration Git & Work Configuration
</span> </span>
</HelpTip>
</div> </div>
{/* Task Type */} {/* Task Type */}
@@ -447,7 +467,9 @@ export function CreateTaskDialog() {
{/* Project — required UNLESS a Product is picked (fan-out task) */} {/* Project — required UNLESS a Product is picked (fan-out task) */}
<div className="space-y-2"> <div className="space-y-2">
<HelpTip label="The repo the branch/PR are opened against once this task is claimed.">
<Label>Project</Label> <Label>Project</Label>
</HelpTip>
<ProjectSelector <ProjectSelector
value={projectId || null} value={projectId || null}
onChange={(value) => setProjectId(value || "")} onChange={(value) => setProjectId(value || "")}
@@ -467,7 +489,9 @@ export function CreateTaskDialog() {
{/* Product (optional) — drives per-cell project routing of subtasks */} {/* Product (optional) — drives per-cell project routing of subtasks */}
<div className="space-y-2"> <div className="space-y-2">
<HelpTip label="Fan-out: each delegated subtask routes to that cell's own mapped project instead of one repo.">
<Label>Product</Label> <Label>Product</Label>
</HelpTip>
<Select <Select
value={productId || "none"} value={productId || "none"}
onValueChange={(v) => setProductId(v === "none" ? "" : v)} onValueChange={(v) => setProductId(v === "none" ? "" : v)}
@@ -497,6 +521,7 @@ export function CreateTaskDialog() {
{/* Actions */} {/* Actions */}
<div className="flex justify-end gap-2 pt-4 border-t"> <div className="flex justify-end gap-2 pt-4 border-t">
<HelpTip label="Discards everything entered above without creating a task.">
<Button <Button
type="button" type="button"
variant="outline" variant="outline"
@@ -507,9 +532,17 @@ export function CreateTaskDialog() {
> >
Cancel Cancel
</Button> </Button>
</HelpTip>
<HelpTip label="Validates the required fields, then creates the task via the API.">
<span
className="inline-block"
tabIndex={createTask.isPending ? 0 : undefined}
>
<Button type="submit" disabled={createTask.isPending}> <Button type="submit" disabled={createTask.isPending}>
{createTask.isPending ? "Creating..." : "Create Task"} {createTask.isPending ? "Creating..." : "Create Task"}
</Button> </Button>
</span>
</HelpTip>
</div> </div>
</form> </form>
</DialogContent> </DialogContent>
@@ -20,6 +20,7 @@ import {
PopoverTrigger, PopoverTrigger,
} from "@/components/ui/popover"; } from "@/components/ui/popover";
import { Search, X, Link2 } from "lucide-react"; import { Search, X, Link2 } from "lucide-react";
import { HelpTip } from "@/components/ui/help-tip";
interface DependencySelectorProps { interface DependencySelectorProps {
selectedIds: string[]; selectedIds: string[];
@@ -68,7 +69,9 @@ export function DependencySelector({
return ( return (
<div className="space-y-2"> <div className="space-y-2">
<HelpTip label="This task cannot be claimed until every dependency listed here reaches a terminal state.">
<Label>Dependencies (optional)</Label> <Label>Dependencies (optional)</Label>
</HelpTip>
{/* Selected dependencies */} {/* Selected dependencies */}
{selectedTasks.length > 0 && ( {selectedTasks.length > 0 && (
@@ -81,9 +84,14 @@ export function DependencySelector({
<div className="flex items-center gap-2 min-w-0"> <div className="flex items-center gap-2 min-w-0">
<Link2 className="h-4 w-4 text-muted-foreground shrink-0" /> <Link2 className="h-4 w-4 text-muted-foreground shrink-0" />
<span className="text-sm truncate">{task.title}</span> <span className="text-sm truncate">{task.title}</span>
<Badge variant="outline" className="font-mono text-xs shrink-0"> <HelpTip label="First 8 characters of this task's full ID.">
<Badge
variant="outline"
className="font-mono text-xs shrink-0"
>
{task.id.slice(0, 8)} {task.id.slice(0, 8)}
</Badge> </Badge>
</HelpTip>
</div> </div>
<Tooltip> <Tooltip>
<TooltipTrigger asChild> <TooltipTrigger asChild>
@@ -106,6 +114,7 @@ export function DependencySelector({
{/* Add dependency popover */} {/* Add dependency popover */}
<Popover open={open} onOpenChange={setOpen}> <Popover open={open} onOpenChange={setOpen}>
<HelpTip label="Only shows tasks that aren't already completed or cancelled — those can't block anything.">
<PopoverTrigger asChild> <PopoverTrigger asChild>
<Button <Button
type="button" type="button"
@@ -116,8 +125,10 @@ export function DependencySelector({
Search tasks to add as dependencies... Search tasks to add as dependencies...
</Button> </Button>
</PopoverTrigger> </PopoverTrigger>
</HelpTip>
<PopoverContent className="w-80 sm:w-96 p-0" align="start"> <PopoverContent className="w-80 sm:w-96 p-0" align="start">
<div className="p-2 border-b"> <div className="p-2 border-b">
<HelpTip label="Matches against task title or ID as you type.">
<Input <Input
value={search} value={search}
onChange={(e) => setSearch(e.target.value)} onChange={(e) => setSearch(e.target.value)}
@@ -125,6 +136,7 @@ export function DependencySelector({
className="h-8" className="h-8"
autoFocus autoFocus
/> />
</HelpTip>
</div> </div>
<ScrollArea className="h-[240px]"> <ScrollArea className="h-[240px]">
{filteredTasks.length === 0 ? ( {filteredTasks.length === 0 ? (
@@ -136,8 +148,15 @@ export function DependencySelector({
{filteredTasks.map((task) => { {filteredTasks.map((task) => {
const isSelected = selectedIds.includes(task.id); const isSelected = selectedIds.includes(task.id);
return ( return (
<Button <HelpTip
key={task.id} key={task.id}
label={
isSelected
? "Click to remove this dependency"
: "Click to add this dependency"
}
>
<Button
type="button" type="button"
variant="ghost" variant="ghost"
className={`w-full h-auto justify-start gap-2 p-2 font-normal whitespace-normal ${ className={`w-full h-auto justify-start gap-2 p-2 font-normal whitespace-normal ${
@@ -166,6 +185,7 @@ export function DependencySelector({
</div> </div>
</div> </div>
</Button> </Button>
</HelpTip>
); );
})} })}
</div> </div>
@@ -81,6 +81,13 @@ export function DocsStatusBadge({
// Full variant with labels // Full variant with labels
return ( return (
<div className={cn("flex flex-col gap-2", className)}> <div className={cn("flex flex-col gap-2", className)}>
<HelpTip
label={
docsComplete
? DOCS_DESCRIPTIONS.complete
: "Blocks the awaiting_documentation → awaiting_pm_review handoff until the documenter marks it done."
}
>
<div className="flex items-center justify-between text-sm"> <div className="flex items-center justify-between text-sm">
<span className="flex items-center gap-1 text-muted-foreground"> <span className="flex items-center gap-1 text-muted-foreground">
<FileCheck className="h-4 w-4" /> <FileCheck className="h-4 w-4" />
@@ -95,6 +102,14 @@ export function DocsStatusBadge({
{docsComplete ? "Complete" : "Pending"} {docsComplete ? "Complete" : "Pending"}
</span> </span>
</div> </div>
</HelpTip>
<HelpTip
label={
prCreated
? PR_DESCRIPTIONS.created
: "The PR is opened before QA review, not after — this task hasn't reached that step yet."
}
>
<div className="flex items-center justify-between text-sm"> <div className="flex items-center justify-between text-sm">
<span className="flex items-center gap-1 text-muted-foreground"> <span className="flex items-center gap-1 text-muted-foreground">
<GitPullRequest className="h-4 w-4" /> <GitPullRequest className="h-4 w-4" />
@@ -109,6 +124,7 @@ export function DocsStatusBadge({
{prCreated ? "Created" : "Pending"} {prCreated ? "Created" : "Pending"}
</span> </span>
</div> </div>
</HelpTip>
</div> </div>
); );
} }
@@ -166,7 +166,9 @@ function EditTaskDialogInner({
<form onSubmit={handleSubmit} className="space-y-6 mt-4"> <form onSubmit={handleSubmit} className="space-y-6 mt-4">
{/* Title */} {/* Title */}
<div className="space-y-2"> <div className="space-y-2">
<HelpTip label="5-200 characters. Shown in the task list, task detail, and used as the default PR title.">
<Label htmlFor="edit-title">Title</Label> <Label htmlFor="edit-title">Title</Label>
</HelpTip>
<Input <Input
id="edit-title" id="edit-title"
value={title} value={title}
@@ -187,7 +189,9 @@ function EditTaskDialogInner({
{/* Team, Priority, Complexity, Nature */} {/* Team, Priority, Complexity, Nature */}
<div className="grid grid-cols-2 md:grid-cols-4 gap-4"> <div className="grid grid-cols-2 md:grid-cols-4 gap-4">
<div className="space-y-2"> <div className="space-y-2">
<HelpTip label="Which cell owns this task — governs claim eligibility. Changing it after a branch exists doesn't rename the branch.">
<Label>Team</Label> <Label>Team</Label>
</HelpTip>
<Select value={team} onValueChange={(v) => setTeam(v as Team)}> <Select value={team} onValueChange={(v) => setTeam(v as Team)}>
<SelectTrigger> <SelectTrigger>
<SelectValue /> <SelectValue />
@@ -202,7 +206,9 @@ function EditTaskDialogInner({
</Select> </Select>
</div> </div>
<div className="space-y-2"> <div className="space-y-2">
<HelpTip label="Orders claim/dispatch queues — P0 tasks surface first. Doesn't force who claims it or block lower priorities.">
<Label>Priority</Label> <Label>Priority</Label>
</HelpTip>
<Select <Select
value={String(priority)} value={String(priority)}
onValueChange={(v) => setPriority(parseInt(v, 10))} onValueChange={(v) => setPriority(parseInt(v, 10))}
@@ -220,7 +226,9 @@ function EditTaskDialogInner({
</Select> </Select>
</div> </div>
<div className="space-y-2"> <div className="space-y-2">
<HelpTip label="Medium/High auto-routes a code task to the cell or Main PM to break down; a bare High dev task with no subtasks self-blocks.">
<Label>Complexity</Label> <Label>Complexity</Label>
</HelpTip>
<Select <Select
value={complexity} value={complexity}
onValueChange={(v) => setComplexity(v as Complexity)} onValueChange={(v) => setComplexity(v as Complexity)}
@@ -238,7 +246,9 @@ function EditTaskDialogInner({
</Select> </Select>
</div> </div>
<div className="space-y-2"> <div className="space-y-2">
<HelpTip label="Filtering only: Non-Technical root tasks awaiting PM review surface in the Board's strategic queue.">
<Label>Nature</Label> <Label>Nature</Label>
</HelpTip>
<Select <Select
value={nature} value={nature}
onValueChange={(v) => setNature(v as TaskNature)} onValueChange={(v) => setNature(v as TaskNature)}
@@ -265,7 +275,9 @@ function EditTaskDialogInner({
type="button" type="button"
className="w-full justify-between" className="w-full justify-between"
> >
<HelpTip label="Agent assignment, target date, task type, and repo routing.">
<span>Advanced Options</span> <span>Advanced Options</span>
</HelpTip>
{advancedOpen ? ( {advancedOpen ? (
<ChevronDown className="h-4 w-4" /> <ChevronDown className="h-4 w-4" />
) : ( ) : (
@@ -276,7 +288,9 @@ function EditTaskDialogInner({
<CollapsibleContent className="space-y-4 pt-4"> <CollapsibleContent className="space-y-4 pt-4">
{/* Assigned To */} {/* Assigned To */}
<div className="space-y-2"> <div className="space-y-2">
<HelpTip label="Pin a specific agent; leave unassigned to let the orchestrator route by role, team, and availability.">
<Label>Assigned To</Label> <Label>Assigned To</Label>
</HelpTip>
<AgentSelector <AgentSelector
value={assignedTo} value={assignedTo}
onChange={setAssignedTo} onChange={setAssignedTo}
@@ -287,7 +301,9 @@ function EditTaskDialogInner({
{/* Target Date */} {/* Target Date */}
<div className="space-y-2"> <div className="space-y-2">
<HelpTip label="Informational deadline only — nothing in the lifecycle enforces or auto-escalates on it.">
<Label>Target Date</Label> <Label>Target Date</Label>
</HelpTip>
<Input <Input
type="datetime-local" type="datetime-local"
value={targetDate} value={targetDate}
@@ -328,7 +344,9 @@ function EditTaskDialogInner({
{/* Project Selector (required - all tasks follow git workflow) */} {/* Project Selector (required - all tasks follow git workflow) */}
<div className="space-y-2"> <div className="space-y-2">
<HelpTip label="The repo the branch/PR are opened against. Locked once a branch exists — see below.">
<Label>Project</Label> <Label>Project</Label>
</HelpTip>
<ProjectSelector <ProjectSelector
value={projectId || null} value={projectId || null}
onChange={(value) => setProjectId(value || "")} onChange={(value) => setProjectId(value || "")}
@@ -347,6 +365,7 @@ function EditTaskDialogInner({
{/* Actions */} {/* Actions */}
<div className="flex justify-end gap-2 pt-4 border-t"> <div className="flex justify-end gap-2 pt-4 border-t">
<HelpTip label="Discards any edits made above and closes without saving.">
<Button <Button
type="button" type="button"
variant="outline" variant="outline"
@@ -354,9 +373,17 @@ function EditTaskDialogInner({
> >
Cancel Cancel
</Button> </Button>
</HelpTip>
<HelpTip label="Sends the changed fields to the API and updates the task in place.">
<span
className="inline-block"
tabIndex={updateTask.isPending ? 0 : undefined}
>
<Button type="submit" disabled={updateTask.isPending}> <Button type="submit" disabled={updateTask.isPending}>
{updateTask.isPending ? "Saving..." : "Save Changes"} {updateTask.isPending ? "Saving..." : "Save Changes"}
</Button> </Button>
</span>
</HelpTip>
</div> </div>
</form> </form>
</DialogContent> </DialogContent>
@@ -52,10 +52,12 @@ export function GitStatusBadge({
if (task.pr_number) { if (task.pr_number) {
return ( return (
<MaybeLink href={task.pr_url ?? pullUrl(repoUrl, task.pr_number)}> <MaybeLink href={task.pr_url ?? pullUrl(repoUrl, task.pr_number)}>
<HelpTip label="Opens this task's pull request on GitHub in a new tab.">
<Badge className="gap-1 text-xs bg-purple-500/10 text-purple-600 dark:text-purple-400"> <Badge className="gap-1 text-xs bg-purple-500/10 text-purple-600 dark:text-purple-400">
<GitPullRequest className="h-3 w-3" /> <GitPullRequest className="h-3 w-3" />
PR #{task.pr_number} PR #{task.pr_number}
</Badge> </Badge>
</HelpTip>
</MaybeLink> </MaybeLink>
); );
} }
@@ -41,12 +41,20 @@ export function MarkdownEditor({
> >
<TabsList className="h-8"> <TabsList className="h-8">
<TabsTrigger value="write" className="text-xs px-2 h-6"> <TabsTrigger value="write" className="text-xs px-2 h-6">
<HelpTip label="Edit the raw markdown text.">
<span className="flex items-center">
<Edit3 className="h-3 w-3 mr-1" /> <Edit3 className="h-3 w-3 mr-1" />
Write Write
</span>
</HelpTip>
</TabsTrigger> </TabsTrigger>
<TabsTrigger value="preview" className="text-xs px-2 h-6"> <TabsTrigger value="preview" className="text-xs px-2 h-6">
<HelpTip label="Renders the markdown as it will appear elsewhere in the app.">
<span className="flex items-center">
<Eye className="h-3 w-3 mr-1" /> <Eye className="h-3 w-3 mr-1" />
Preview Preview
</span>
</HelpTip>
</TabsTrigger> </TabsTrigger>
</TabsList> </TabsList>
</Tabs> </Tabs>
@@ -76,9 +84,11 @@ export function MarkdownEditor({
<span>Markdown supported</span> <span>Markdown supported</span>
</HelpTip> </HelpTip>
{minLength && ( {minLength && (
<HelpTip label="Submission is blocked until the count reaches this minimum.">
<span className={value.length < minLength ? "text-destructive" : ""}> <span className={value.length < minLength ? "text-destructive" : ""}>
{value.length}/{minLength} min characters {value.length}/{minLength} min characters
</span> </span>
</HelpTip>
)} )}
</div> </div>
@@ -41,6 +41,7 @@ import {
import { toast } from "sonner"; import { toast } from "sonner";
import { EditTaskDialog } from "./edit-task-dialog"; import { EditTaskDialog } from "./edit-task-dialog";
import { RequiredNotesDialog } from "./task-detail/task-action-dialogs"; import { RequiredNotesDialog } from "./task-detail/task-action-dialogs";
import { HelpTip } from "@/components/ui/help-tip";
interface TaskActionsProps { interface TaskActionsProps {
task: Task; task: Task;
@@ -169,59 +170,77 @@ export function TaskActions({
<DropdownMenuContent align="end"> <DropdownMenuContent align="end">
{/* Edit option */} {/* Edit option */}
{showEdit && canEdit && ( {showEdit && canEdit && (
<HelpTip label="Opens the edit dialog to change title, description, team, priority, and more.">
<DropdownMenuItem onClick={() => setEditOpen(true)}> <DropdownMenuItem onClick={() => setEditOpen(true)}>
<Pencil className="h-4 w-4 mr-2" /> <Pencil className="h-4 w-4 mr-2" />
Edit Edit
</DropdownMenuItem> </DropdownMenuItem>
</HelpTip>
)} )}
{/* Backlog status info */} {/* Backlog status info */}
{isBacklog && ( {isBacklog && (
<> <>
<HelpTip label="Backlog tasks need PM setup (dependencies/session) before they become claimable.">
<span className="block" tabIndex={0}>
<DropdownMenuItem disabled className="text-muted-foreground"> <DropdownMenuItem disabled className="text-muted-foreground">
<Clock className="h-4 w-4 mr-2" /> <Clock className="h-4 w-4 mr-2" />
Awaiting PM Activation Awaiting PM Activation
</DropdownMenuItem> </DropdownMenuItem>
</span>
</HelpTip>
<DropdownMenuSeparator /> <DropdownMenuSeparator />
</> </>
)} )}
{/* Lifecycle actions */} {/* Lifecycle actions */}
{task.status === TaskStatus.PENDING && ( {task.status === TaskStatus.PENDING && (
<HelpTip label="Locks this task to you (pending → claimed).">
<DropdownMenuItem onClick={() => handleAction("claim")}> <DropdownMenuItem onClick={() => handleAction("claim")}>
<Play className="h-4 w-4 mr-2" /> <Play className="h-4 w-4 mr-2" />
Claim Claim
</DropdownMenuItem> </DropdownMenuItem>
</HelpTip>
)} )}
{task.status === TaskStatus.CLAIMED && ( {task.status === TaskStatus.CLAIMED && (
<HelpTip label="Begins active work (claimed → in_progress).">
<DropdownMenuItem onClick={() => handleAction("start")}> <DropdownMenuItem onClick={() => handleAction("start")}>
<Play className="h-4 w-4 mr-2" /> <Play className="h-4 w-4 mr-2" />
Start Start
</DropdownMenuItem> </DropdownMenuItem>
</HelpTip>
)} )}
{task.status === TaskStatus.IN_PROGRESS && ( {task.status === TaskStatus.IN_PROGRESS && (
<HelpTip label="Stops work temporarily; can be resumed later (in_progress → paused).">
<DropdownMenuItem onClick={() => handleAction("pause")}> <DropdownMenuItem onClick={() => handleAction("pause")}>
<Pause className="h-4 w-4 mr-2" /> <Pause className="h-4 w-4 mr-2" />
Pause Pause
</DropdownMenuItem> </DropdownMenuItem>
</HelpTip>
)} )}
{task.status === TaskStatus.PAUSED && ( {task.status === TaskStatus.PAUSED && (
<HelpTip label="Resumes paused work (paused → in_progress).">
<DropdownMenuItem onClick={() => handleAction("resume")}> <DropdownMenuItem onClick={() => handleAction("resume")}>
<Play className="h-4 w-4 mr-2" /> <Play className="h-4 w-4 mr-2" />
Resume Resume
</DropdownMenuItem> </DropdownMenuItem>
</HelpTip>
)} )}
{task.status === TaskStatus.BLOCKED && ( {task.status === TaskStatus.BLOCKED && (
<HelpTip label="Clears the block and restores the original owner (blocked → in_progress or pending).">
<DropdownMenuItem onClick={() => handleAction("unblock")}> <DropdownMenuItem onClick={() => handleAction("unblock")}>
<Play className="h-4 w-4 mr-2" /> <Play className="h-4 w-4 mr-2" />
Unblock Unblock
</DropdownMenuItem> </DropdownMenuItem>
</HelpTip>
)} )}
{task.status === TaskStatus.CANCELLED && ( {task.status === TaskStatus.CANCELLED && (
<HelpTip label="Resurrects a cancelled task back to pending — an audited privileged override.">
<DropdownMenuItem onClick={() => handleAction("reopen")}> <DropdownMenuItem onClick={() => handleAction("reopen")}>
<Play className="h-4 w-4 mr-2" /> <Play className="h-4 w-4 mr-2" />
Reopen Reopen
</DropdownMenuItem> </DropdownMenuItem>
</HelpTip>
)} )}
{task.status !== TaskStatus.COMPLETED && {task.status !== TaskStatus.COMPLETED &&
@@ -230,10 +249,13 @@ export function TaskActions({
<> <>
<DropdownMenuSeparator /> <DropdownMenuSeparator />
<HelpTip label="Directly completes the task — requires the normal review chain done, or this is your own PM root.">
<DropdownMenuItem onClick={() => handleAction("complete")}> <DropdownMenuItem onClick={() => handleAction("complete")}>
<CheckCircle className="h-4 w-4 mr-2" /> <CheckCircle className="h-4 w-4 mr-2" />
Complete Complete
</DropdownMenuItem> </DropdownMenuItem>
</HelpTip>
<HelpTip label="Cancels this task and every subtask beneath it; the reason becomes the audit record.">
<DropdownMenuItem <DropdownMenuItem
onClick={() => handleAction("cancel")} onClick={() => handleAction("cancel")}
className="text-orange-600" className="text-orange-600"
@@ -241,11 +263,13 @@ export function TaskActions({
<XCircle className="h-4 w-4 mr-2" /> <XCircle className="h-4 w-4 mr-2" />
Cancel Cancel
</DropdownMenuItem> </DropdownMenuItem>
</HelpTip>
</> </>
)} )}
{/* Cancel for backlog tasks (allowed - can remove from backlog) */} {/* Cancel for backlog tasks (allowed - can remove from backlog) */}
{isBacklog && ( {isBacklog && (
<HelpTip label="Removes this task from the backlog and cancels any subtasks; the reason becomes the audit record.">
<DropdownMenuItem <DropdownMenuItem
onClick={() => handleAction("cancel")} onClick={() => handleAction("cancel")}
className="text-orange-600" className="text-orange-600"
@@ -253,12 +277,14 @@ export function TaskActions({
<XCircle className="h-4 w-4 mr-2" /> <XCircle className="h-4 w-4 mr-2" />
Cancel Cancel
</DropdownMenuItem> </DropdownMenuItem>
</HelpTip>
)} )}
{/* Delete option */} {/* Delete option */}
{showDelete && canDelete && ( {showDelete && canDelete && (
<> <>
<DropdownMenuSeparator /> <DropdownMenuSeparator />
<HelpTip label="Permanently deletes this task and every subtask beneath it — cannot be undone.">
<DropdownMenuItem <DropdownMenuItem
onClick={() => setDeleteOpen(true)} onClick={() => setDeleteOpen(true)}
className="text-red-600" className="text-red-600"
@@ -266,6 +292,7 @@ export function TaskActions({
<Trash2 className="h-4 w-4 mr-2" /> <Trash2 className="h-4 w-4 mr-2" />
Delete Delete
</DropdownMenuItem> </DropdownMenuItem>
</HelpTip>
</> </>
)} )}
</DropdownMenuContent> </DropdownMenuContent>
@@ -314,13 +341,17 @@ export function TaskActions({
</AlertDialogDescription> </AlertDialogDescription>
</AlertDialogHeader> </AlertDialogHeader>
<AlertDialogFooter> <AlertDialogFooter>
<HelpTip label="Closes without deleting anything.">
<AlertDialogCancel>Cancel</AlertDialogCancel> <AlertDialogCancel>Cancel</AlertDialogCancel>
</HelpTip>
<HelpTip label="Deletes the task and cascades to every subtask beneath it, leaf-first.">
<AlertDialogAction <AlertDialogAction
onClick={handleDelete} onClick={handleDelete}
className="bg-red-600 hover:bg-red-700" className="bg-red-600 hover:bg-red-700"
> >
{deleteTask.isPending ? "Deleting..." : "Delete"} {deleteTask.isPending ? "Deleting..." : "Delete"}
</AlertDialogAction> </AlertDialogAction>
</HelpTip>
</AlertDialogFooter> </AlertDialogFooter>
</AlertDialogContent> </AlertDialogContent>
</AlertDialog> </AlertDialog>
@@ -44,11 +44,15 @@ describe("TabProgress progress-update collapse", () => {
const task = buildTask({ progress_updates: makeUpdates(30) }); const task = buildTask({ progress_updates: makeUpdates(30) });
const { container } = render(<TabProgress task={task} />); const { container } = render(<TabProgress task={task} />);
// Each update's collapse trigger is the only <button> in its <li> that // Scope to the collapse trigger specifically via its test id — the row
// carries Radix's data-state attribute; list order matches sort order // also carries a delete button wrapped in a HelpTip/Tooltip, which
// (newest first), so the first two entries are the 2 most recent. // stamps its own Radix data-state, so a bare "button[data-state]"
// selector would match both. List order matches sort order (newest
// first), so the first two entries are the 2 most recent.
const triggers = Array.from( const triggers = Array.from(
container.querySelectorAll("li button[data-state]"), container.querySelectorAll(
'li button[data-testid="progress-update-trigger"]',
),
); );
expect(triggers).toHaveLength(30); expect(triggers).toHaveLength(30);
expect(triggers[0]).toHaveAttribute("data-state", "open"); expect(triggers[0]).toHaveAttribute("data-state", "open");
@@ -54,8 +54,14 @@ describe("Task detail readability: long progress history + long criteria list",
const task = buildTask({ progress_updates: makeUpdates(32) }); const task = buildTask({ progress_updates: makeUpdates(32) });
const { container } = render(<TabProgress task={task} />); const { container } = render(<TabProgress task={task} />);
// Scope to the collapse trigger specifically via its test id — the row
// also carries a delete button wrapped in a HelpTip/Tooltip, which
// stamps its own Radix data-state, so a bare "button[data-state]"
// selector would match both.
const triggers = Array.from( const triggers = Array.from(
container.querySelectorAll("li button[data-state]"), container.querySelectorAll(
'li button[data-testid="progress-update-trigger"]',
),
); );
expect(triggers).toHaveLength(32); expect(triggers).toHaveLength(32);
const openCount = triggers.filter( const openCount = triggers.filter(
@@ -180,19 +180,27 @@ export function AcceptanceCriteria({ task }: AcceptanceCriteriaProps) {
return ( return (
<CollapsibleSection <CollapsibleSection
title="Acceptance Criteria" title={
<HelpTip label="What proves this task is done — reviewers and the auto-submit gate check coverage against these">
<span className="w-fit">Acceptance Criteria</span>
</HelpTip>
}
content={criteriaText} content={criteriaText}
open={isAdding || editingIndex !== null ? true : undefined} open={isAdding || editingIndex !== null ? true : undefined}
actions={ actions={
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<span className="text-sm text-muted-foreground"> <HelpTip label="Criteria whose checkbox is checked, out of the total">
<span className="text-sm text-muted-foreground w-fit">
{completedCount}/{criteria.length} completed {completedCount}/{criteria.length} completed
</span> </span>
</HelpTip>
{!isAdding && ( {!isAdding && (
<HelpTip label="Adds a new empty criterion to this list">
<Button size="sm" variant="ghost" onClick={() => setIsAdding(true)}> <Button size="sm" variant="ghost" onClick={() => setIsAdding(true)}>
<Plus className="h-4 w-4 mr-1" /> <Plus className="h-4 w-4 mr-1" />
Add Add
</Button> </Button>
</HelpTip>
)} )}
</div> </div>
} }
@@ -5,6 +5,7 @@ import { Card, CardContent } from "@/components/ui/card";
import { Bookmark, Clock, User, ListTodo, FileText } from "lucide-react"; import { Bookmark, Clock, User, ListTodo, FileText } from "lucide-react";
import { getAgentDisplayName } from "@/lib/agent-utils"; import { getAgentDisplayName } from "@/lib/agent-utils";
import { formatAbsoluteTimestamp } from "@/lib/utils"; import { formatAbsoluteTimestamp } from "@/lib/utils";
import { HelpTip } from "@/components/ui/help-tip";
interface CheckpointCardProps { interface CheckpointCardProps {
checkpoint: Checkpoint; checkpoint: Checkpoint;
@@ -14,10 +15,12 @@ export function CheckpointCard({ checkpoint }: CheckpointCardProps) {
return ( return (
<Card className="overflow-hidden"> <Card className="overflow-hidden">
<div className="bg-primary/10 px-4 py-2 flex items-center justify-between"> <div className="bg-primary/10 px-4 py-2 flex items-center justify-between">
<HelpTip label="A saved state snapshot an agent leaves so work can resume later without re-deriving context">
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<Bookmark className="h-4 w-4 text-primary" /> <Bookmark className="h-4 w-4 text-primary" />
<span className="font-medium text-sm">Checkpoint</span> <span className="font-medium text-sm">Checkpoint</span>
</div> </div>
</HelpTip>
<div className="flex items-center gap-2 text-xs text-muted-foreground"> <div className="flex items-center gap-2 text-xs text-muted-foreground">
<Clock className="h-3 w-3" /> <Clock className="h-3 w-3" />
{formatAbsoluteTimestamp(checkpoint.timestamp)} {formatAbsoluteTimestamp(checkpoint.timestamp)}
@@ -32,7 +35,9 @@ export function CheckpointCard({ checkpoint }: CheckpointCardProps) {
{/* State Summary */} {/* State Summary */}
<div className="mb-4"> <div className="mb-4">
<h4 className="text-sm font-medium mb-1">State Summary</h4> <HelpTip label="What the agent understood to be true about the task when this checkpoint was saved">
<h4 className="text-sm font-medium mb-1 w-fit">State Summary</h4>
</HelpTip>
<p className="text-sm text-muted-foreground whitespace-pre-wrap"> <p className="text-sm text-muted-foreground whitespace-pre-wrap">
{checkpoint.state_summary} {checkpoint.state_summary}
</p> </p>
@@ -41,10 +46,12 @@ export function CheckpointCard({ checkpoint }: CheckpointCardProps) {
{/* Remaining Work */} {/* Remaining Work */}
{checkpoint.remaining_work.length > 0 && ( {checkpoint.remaining_work.length > 0 && (
<div className="mb-4"> <div className="mb-4">
<div className="flex items-center gap-2 mb-2"> <HelpTip label="Sub-steps the agent identified as still outstanding at checkpoint time">
<div className="flex items-center gap-2 mb-2 w-fit">
<ListTodo className="h-4 w-4 text-muted-foreground" /> <ListTodo className="h-4 w-4 text-muted-foreground" />
<h4 className="text-sm font-medium">Remaining Work</h4> <h4 className="text-sm font-medium">Remaining Work</h4>
</div> </div>
</HelpTip>
<ul className="list-disc list-inside text-sm text-muted-foreground space-y-1"> <ul className="list-disc list-inside text-sm text-muted-foreground space-y-1">
{checkpoint.remaining_work.map((item, idx) => ( {checkpoint.remaining_work.map((item, idx) => (
<li key={idx}>{item}</li> <li key={idx}>{item}</li>
@@ -56,10 +63,12 @@ export function CheckpointCard({ checkpoint }: CheckpointCardProps) {
{/* Notes */} {/* Notes */}
{checkpoint.notes && ( {checkpoint.notes && (
<div> <div>
<div className="flex items-center gap-2 mb-2"> <HelpTip label="Free-form notes the checkpointing agent left for whoever resumes this task">
<div className="flex items-center gap-2 mb-2 w-fit">
<FileText className="h-4 w-4 text-muted-foreground" /> <FileText className="h-4 w-4 text-muted-foreground" />
<h4 className="text-sm font-medium">Notes</h4> <h4 className="text-sm font-medium">Notes</h4>
</div> </div>
</HelpTip>
<p className="text-sm text-muted-foreground whitespace-pre-wrap"> <p className="text-sm text-muted-foreground whitespace-pre-wrap">
{checkpoint.notes} {checkpoint.notes}
</p> </p>
@@ -6,6 +6,7 @@ import { Progress } from "@/components/ui/progress";
import { MessageSquare, Clock } from "lucide-react"; import { MessageSquare, Clock } from "lucide-react";
import { getAgentDisplayName } from "@/lib/agent-utils"; import { getAgentDisplayName } from "@/lib/agent-utils";
import { formatAbsoluteTimestamp } from "@/lib/utils"; import { formatAbsoluteTimestamp } from "@/lib/utils";
import { HelpTip } from "@/components/ui/help-tip";
interface ProgressTimelineProps { interface ProgressTimelineProps {
updates: ProgressUpdate[]; updates: ProgressUpdate[];
@@ -45,17 +46,21 @@ export function ProgressTimeline({ updates }: ProgressTimelineProps) {
<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">Progress Updates</CardTitle> <HelpTip label="Timestamped status updates posted while agents work this task, newest first">
<CardTitle className="text-lg w-fit">Progress Updates</CardTitle>
</HelpTip>
<span className="text-sm text-muted-foreground"> <span className="text-sm text-muted-foreground">
{updates.length} update{updates.length !== 1 ? "s" : ""} {updates.length} update{updates.length !== 1 ? "s" : ""}
</span> </span>
</div> </div>
{latestWithPercentage && ( {latestWithPercentage && (
<div className="mt-2"> <div className="mt-2">
<HelpTip label="The most recent percentage reported — self-reported or derived from completed plan steps">
<div className="flex items-center justify-between text-sm mb-1"> <div className="flex items-center justify-between text-sm mb-1">
<span className="text-muted-foreground">Overall Progress</span> <span className="text-muted-foreground">Overall Progress</span>
<span className="font-medium">{currentProgress}%</span> <span className="font-medium">{currentProgress}%</span>
</div> </div>
</HelpTip>
<Progress value={currentProgress} className="h-2" /> <Progress value={currentProgress} className="h-2" />
</div> </div>
)} )}
@@ -100,9 +105,11 @@ export function ProgressTimeline({ updates }: ProgressTimelineProps) {
value={update.percentage} value={update.percentage}
className="h-1.5 flex-1" className="h-1.5 flex-1"
/> />
<HelpTip label="This update's reported completion percentage, out of 100">
<span className="text-xs text-muted-foreground w-10 text-right"> <span className="text-xs text-muted-foreground w-10 text-right">
{update.percentage}% {update.percentage}%
</span> </span>
</HelpTip>
</div> </div>
</div> </div>
)} )}
@@ -65,7 +65,8 @@ export function SubtasksList({ task }: SubtasksListProps) {
<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="Direct children of this task; badge is completed/total">
<CardTitle className="text-lg flex items-center gap-2 w-fit">
<ListTree className="h-5 w-5" /> <ListTree className="h-5 w-5" />
Subtasks Subtasks
{totalCount > 0 && ( {totalCount > 0 && (
@@ -74,12 +75,14 @@ export function SubtasksList({ task }: SubtasksListProps) {
</Badge> </Badge>
)} )}
</CardTitle> </CardTitle>
</HelpTip>
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
{totalCount > 0 && ( {totalCount > 0 && (
<span className="text-sm text-muted-foreground"> <span className="text-sm text-muted-foreground">
{completionPercent}% complete {completionPercent}% complete
</span> </span>
)} )}
<HelpTip label="Opens the Tasks list, pre-filtered to this parent and team, to create a new subtask">
<Link <Link
href={`/tasks?parent=${task.id}&team=${task.team}`} href={`/tasks?parent=${task.id}&team=${task.team}`}
prefetch={false} prefetch={false}
@@ -89,6 +92,7 @@ export function SubtasksList({ task }: SubtasksListProps) {
Add Add
</Button> </Button>
</Link> </Link>
</HelpTip>
</div> </div>
</div> </div>
</CardHeader> </CardHeader>
@@ -77,7 +77,9 @@ function SiblingCard({ sib }: { sib: CollisionSibling }) {
</code> </code>
)} )}
{sib.pr_number != null && ( {sib.pr_number != null && (
<HelpTip label="This sibling's pull request number, if one has been opened yet">
<Badge variant="outline">#{sib.pr_number}</Badge> <Badge variant="outline">#{sib.pr_number}</Badge>
</HelpTip>
)} )}
{sib.adds_migration && ( {sib.adds_migration && (
<HelpTip label={MIGRATION_TIP}> <HelpTip label={MIGRATION_TIP}>
@@ -130,9 +132,11 @@ function SiblingCard({ sib }: { sib: CollisionSibling }) {
{sib.undeclared.length > 0 && ( {sib.undeclared.length > 0 && (
<div className="rounded border border-amber-300 dark:border-amber-800 bg-amber-50 dark:bg-amber-950/40 p-2 text-xs"> <div className="rounded border border-amber-300 dark:border-amber-800 bg-amber-50 dark:bg-amber-950/40 p-2 text-xs">
<span className="text-amber-700 dark:text-amber-300 font-medium"> <HelpTip label="Files this sibling actually touched in commits but never declared in its collision surface">
<span className="text-amber-700 dark:text-amber-300 font-medium w-fit">
Drift touched but not declared: Drift touched but not declared:
</span> </span>
</HelpTip>
<div className="mt-1 flex flex-wrap gap-1"> <div className="mt-1 flex flex-wrap gap-1">
{sib.undeclared.map((f) => ( {sib.undeclared.map((f) => (
<code <code
@@ -197,9 +201,11 @@ export function TabCollision({ task }: TabCollisionProps) {
<div className="space-y-4"> <div className="space-y-4">
<DeclaredSurfaceCard data={data} /> <DeclaredSurfaceCard data={data} />
<div> <div>
<h3 className="text-sm font-semibold mb-3"> <HelpTip label="Siblings whose declared file surface overlaps yours, or that share a migration/shared-file chain">
<h3 className="text-sm font-semibold mb-3 w-fit">
{siblings.length} colliding sibling{siblings.length > 1 ? "s" : ""} {siblings.length} colliding sibling{siblings.length > 1 ? "s" : ""}
</h3> </h3>
</HelpTip>
<div className="space-y-3"> <div className="space-y-3">
{siblings.map((sib) => ( {siblings.map((sib) => (
<SiblingCard key={sib.id} sib={sib} /> <SiblingCard key={sib.id} sib={sib} />
@@ -218,7 +224,9 @@ function DeclaredSurfaceCard({ data }: { data: CollisionMap }) {
return ( return (
<Card> <Card>
<CardContent className="pt-4 space-y-2"> <CardContent className="pt-4 space-y-2">
<h3 className="text-sm font-semibold">Declared surface</h3> <HelpTip label="The file globs, migration flag, and shared-surface flag this task pre-declared for sequencing">
<h3 className="text-sm font-semibold w-fit">Declared surface</h3>
</HelpTip>
{hasSurface ? ( {hasSurface ? (
<> <>
{data.intends_to_touch.length > 0 && ( {data.intends_to_touch.length > 0 && (
@@ -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 { formatAbsoluteTimestamp } from "@/lib/utils";
import { HelpTip } from "@/components/ui/help-tip"; import { HelpTip } from "@/components/ui/help-tip";
interface TabCommitsProps { interface TabCommitsProps {
@@ -114,13 +115,15 @@ export function TabCommits({ task }: TabCommitsProps) {
<Card> <Card>
<CardHeader className="pb-3"> <CardHeader className="pb-3">
<div className="grid grid-cols-3 items-center gap-4"> <div className="grid grid-cols-3 items-center gap-4">
<CardTitle className="text-lg flex items-center gap-2"> <HelpTip label="Commits attributed to this task — new ones link automatically as devs push">
<CardTitle className="text-lg flex items-center gap-2 w-fit">
<GitCommit className="h-5 w-5" /> <GitCommit className="h-5 w-5" />
Linked Commits Linked Commits
<span className="text-sm font-normal text-muted-foreground"> <span className="text-sm font-normal text-muted-foreground">
({commits.length}) ({commits.length})
</span> </span>
</CardTitle> </CardTitle>
</HelpTip>
<div className="flex items-center justify-center gap-2"> <div className="flex items-center justify-center gap-2">
{task.branch_name && ( {task.branch_name && (
<HelpTip label="Git branch this task is being worked on"> <HelpTip label="Git branch this task is being worked on">
@@ -149,6 +152,7 @@ export function TabCommits({ task }: TabCommitsProps) {
</div> </div>
<div className="flex justify-end"> <div className="flex justify-end">
{!isAdding && ( {!isAdding && (
<HelpTip label="Manually attach a commit hash — normally commits link automatically via the commit verb">
<Button <Button
size="sm" size="sm"
variant="ghost" variant="ghost"
@@ -157,6 +161,7 @@ export function TabCommits({ task }: TabCommitsProps) {
<Plus className="h-4 w-4 mr-1" /> <Plus className="h-4 w-4 mr-1" />
Link Link
</Button> </Button>
</HelpTip>
)} )}
</div> </div>
</div> </div>
@@ -167,9 +172,11 @@ export function TabCommits({ task }: TabCommitsProps) {
<div className="border rounded-lg p-4 mb-4 space-y-3"> <div className="border rounded-lg p-4 mb-4 space-y-3">
<div className="grid grid-cols-2 gap-3"> <div className="grid grid-cols-2 gap-3">
<div> <div>
<label className="text-sm font-medium mb-1 block"> <HelpTip label="7-40 character git SHA; a short hash is fine">
<label className="text-sm font-medium mb-1 block w-fit">
Commit Hash Commit Hash
</label> </label>
</HelpTip>
<Input <Input
ref={hashRef} ref={hashRef}
value={newHash} value={newHash}
@@ -179,9 +186,11 @@ export function TabCommits({ task }: TabCommitsProps) {
/> />
</div> </div>
<div> <div>
<label className="text-sm font-medium mb-1 block"> <HelpTip label="Free-text description shown wherever this commit is displayed">
<label className="text-sm font-medium mb-1 block w-fit">
Commit Message Commit Message
</label> </label>
</HelpTip>
<Input <Input
value={newMessage} value={newMessage}
onChange={(e) => setNewMessage(e.target.value)} onChange={(e) => setNewMessage(e.target.value)}
@@ -224,10 +233,12 @@ export function TabCommits({ task }: TabCommitsProps) {
<p className="text-sm mt-2 mb-4"> <p className="text-sm mt-2 mb-4">
Commits will be linked as developers push code for this task. Commits will be linked as developers push code for this task.
</p> </p>
<HelpTip label="Manually attach a commit hash if auto-linking missed one">
<Button variant="outline" onClick={() => setIsAdding(true)}> <Button variant="outline" onClick={() => setIsAdding(true)}>
<Plus className="h-4 w-4 mr-2" /> <Plus className="h-4 w-4 mr-2" />
Link Commit Manually Link Commit Manually
</Button> </Button>
</HelpTip>
</div> </div>
) : ( ) : (
<div className="space-y-3"> <div className="space-y-3">
@@ -266,10 +277,12 @@ export function TabCommits({ task }: TabCommitsProps) {
)} )}
{/* Time */} {/* Time */}
<HelpTip label={formatAbsoluteTimestamp(commit.timestamp)}>
<span className="flex items-center gap-1"> <span className="flex items-center gap-1">
<Clock className="h-3 w-3" /> <Clock className="h-3 w-3" />
{formatTime(commit.timestamp)} {formatTime(commit.timestamp)}
</span> </span>
</HelpTip>
</div> </div>
</div> </div>
</div> </div>
@@ -26,6 +26,17 @@ interface TabDependenciesProps {
task: Task; task: Task;
} }
// Grounded in roboco/services/gateway/claim_guards.py: dependency_ids
// genuinely gates claim (a task can't be claimed until every dependency
// reaches completed/cancelled); blocker_ids is the pure-informational
// inverse ("tasks this one is blocking") and does not itself gate anything.
const FIELD_TIPS: Record<DependencyListProps["field"], string> = {
dependency_ids:
"Tasks that must reach completed/cancelled before this one can be claimed.",
blocker_ids:
"Tasks that depend on this one — informational; does not block this task's own claim.",
};
interface DependencyListProps { interface DependencyListProps {
task: Task; task: Task;
field: "dependency_ids" | "blocker_ids"; field: "dependency_ids" | "blocker_ids";
@@ -124,19 +135,33 @@ function DependencyList({
<CardHeader> <CardHeader>
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
<CardTitle className="text-lg flex items-center gap-2"> <CardTitle className="text-lg flex items-center gap-2">
<HelpTip label={FIELD_TIPS[field]}>
<span className="flex items-center gap-2">
{icon} {icon}
{title} {title}
</span>
</HelpTip>
{ids.length > 0 && field === "blocker_ids" && ( {ids.length > 0 && field === "blocker_ids" && (
<HelpTip label="Number of other tasks currently depending on this one">
<Badge variant="destructive" className="ml-2"> <Badge variant="destructive" className="ml-2">
{ids.length} {ids.length}
</Badge> </Badge>
</HelpTip>
)} )}
</CardTitle> </CardTitle>
{!isAdding && ( {!isAdding && (
<HelpTip
label={
field === "dependency_ids"
? "Add a task ID this task must wait on"
: "Add a task ID that this task is blocking"
}
>
<Button size="sm" variant="ghost" onClick={() => setIsAdding(true)}> <Button size="sm" variant="ghost" onClick={() => setIsAdding(true)}>
<Plus className="h-4 w-4 mr-1" /> <Plus className="h-4 w-4 mr-1" />
Add Add
</Button> </Button>
</HelpTip>
)} )}
</div> </div>
</CardHeader> </CardHeader>
@@ -167,9 +192,17 @@ function DependencyList({
</span> </span>
</Link> </Link>
</HelpTip> </HelpTip>
<HelpTip
label={
field === "dependency_ids"
? "Click the id to open this dependency task"
: "This task is blocking the linked task's claim"
}
>
<Badge variant="outline" className={badgeClass}> <Badge variant="outline" className={badgeClass}>
{badgeLabel} {badgeLabel}
</Badge> </Badge>
</HelpTip>
<HelpTip label="Remove this dependency"> <HelpTip label="Remove this dependency">
<Button <Button
size="sm" size="sm"
@@ -198,6 +231,7 @@ function DependencyList({
className="h-8 text-sm flex-1 font-mono" className="h-8 text-sm flex-1 font-mono"
disabled={updateTask.isPending} disabled={updateTask.isPending}
/> />
<HelpTip label="Discard and cancel">
<Button <Button
size="sm" size="sm"
variant="ghost" variant="ghost"
@@ -209,6 +243,14 @@ function DependencyList({
> >
<X className="h-4 w-4" /> <X className="h-4 w-4" />
</Button> </Button>
</HelpTip>
<HelpTip
label={
field === "dependency_ids"
? "Add this task ID as a dependency"
: "Add this task ID as a blocked task"
}
>
<Button <Button
size="sm" size="sm"
onClick={handleAdd} onClick={handleAdd}
@@ -217,6 +259,7 @@ function DependencyList({
> >
<Check className="h-4 w-4" /> <Check className="h-4 w-4" />
</Button> </Button>
</HelpTip>
</li> </li>
)} )}
</ul> </ul>
@@ -367,8 +410,12 @@ export function TabDependencies({ task }: TabDependenciesProps) {
<CardHeader> <CardHeader>
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
<CardTitle className="text-lg flex items-center gap-2"> <CardTitle className="text-lg flex items-center gap-2">
<HelpTip label="The coordination task this one was delegated under, if any">
<span className="flex items-center gap-2">
<ArrowUp className="h-5 w-5 text-purple-500" /> <ArrowUp className="h-5 w-5 text-purple-500" />
Parent Task Parent Task
</span>
</HelpTip>
</CardTitle> </CardTitle>
</div> </div>
</CardHeader> </CardHeader>
@@ -386,6 +433,7 @@ export function TabDependencies({ task }: TabDependenciesProps) {
className="h-8 text-sm flex-1 font-mono" className="h-8 text-sm flex-1 font-mono"
disabled={updateTask.isPending} disabled={updateTask.isPending}
/> />
<HelpTip label="Discard and cancel">
<Button <Button
size="sm" size="sm"
variant="ghost" variant="ghost"
@@ -397,6 +445,8 @@ export function TabDependencies({ task }: TabDependenciesProps) {
> >
<X className="h-4 w-4" /> <X className="h-4 w-4" />
</Button> </Button>
</HelpTip>
<HelpTip label="Save this parent task ID">
<Button <Button
size="sm" size="sm"
onClick={handleParentSave} onClick={handleParentSave}
@@ -406,6 +456,7 @@ export function TabDependencies({ task }: TabDependenciesProps) {
> >
<Check className="h-4 w-4" /> <Check className="h-4 w-4" />
</Button> </Button>
</HelpTip>
</div> </div>
) : task.parent_task_id ? ( ) : task.parent_task_id ? (
<div <div
@@ -427,6 +478,7 @@ export function TabDependencies({ task }: TabDependenciesProps) {
<Badge variant="outline" className="ml-auto"> <Badge variant="outline" className="ml-auto">
View Parent View Parent
</Badge> </Badge>
<HelpTip label="Opens editing to change or clear the parent task — does not delete anything">
<Button <Button
size="sm" size="sm"
variant="ghost" variant="ghost"
@@ -438,6 +490,7 @@ export function TabDependencies({ task }: TabDependenciesProps) {
> >
<Trash2 className="h-3 w-3 text-destructive" /> <Trash2 className="h-3 w-3 text-destructive" />
</Button> </Button>
</HelpTip>
</div> </div>
) : ( ) : (
<p <p
@@ -455,8 +508,12 @@ export function TabDependencies({ task }: TabDependenciesProps) {
<CardHeader> <CardHeader>
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
<CardTitle className="text-lg flex items-center gap-2"> <CardTitle className="text-lg flex items-center gap-2">
<HelpTip label="A same-parent sibling with a strictly lower sequence must go terminal before this one can be claimed; ties run in parallel">
<span className="flex items-center gap-2">
<Hash className="h-5 w-5 text-amber-500" /> <Hash className="h-5 w-5 text-amber-500" />
Sequence Sequence
</span>
</HelpTip>
</CardTitle> </CardTitle>
</div> </div>
</CardHeader> </CardHeader>
@@ -475,6 +532,7 @@ export function TabDependencies({ task }: TabDependenciesProps) {
className="h-8 text-sm flex-1" className="h-8 text-sm flex-1"
disabled={updateTask.isPending} disabled={updateTask.isPending}
/> />
<HelpTip label="Discard and cancel">
<Button <Button
size="sm" size="sm"
variant="ghost" variant="ghost"
@@ -483,6 +541,8 @@ export function TabDependencies({ task }: TabDependenciesProps) {
> >
<X className="h-4 w-4" /> <X className="h-4 w-4" />
</Button> </Button>
</HelpTip>
<HelpTip label="Save this sequence number">
<Button <Button
size="sm" size="sm"
onClick={handleSequenceSave} onClick={handleSequenceSave}
@@ -492,6 +552,7 @@ export function TabDependencies({ task }: TabDependenciesProps) {
> >
<Check className="h-4 w-4" /> <Check className="h-4 w-4" />
</Button> </Button>
</HelpTip>
</div> </div>
) : ( ) : (
<div <div
@@ -504,6 +565,7 @@ export function TabDependencies({ task }: TabDependenciesProps) {
<span className="ml-2 text-xs text-muted-foreground"> <span className="ml-2 text-xs text-muted-foreground">
Order within siblings lower runs first Order within siblings lower runs first
</span> </span>
<HelpTip label="Edit the sequence number">
<Button <Button
size="sm" size="sm"
variant="ghost" variant="ghost"
@@ -515,6 +577,7 @@ export function TabDependencies({ task }: TabDependenciesProps) {
> >
<Pencil className="h-3 w-3" /> <Pencil className="h-3 w-3" />
</Button> </Button>
</HelpTip>
</div> </div>
)} )}
</CardContent> </CardContent>
@@ -119,7 +119,11 @@ function FindingCard({
</div> </div>
{finding.evidence && ( {finding.evidence && (
<details className="text-xs text-muted-foreground"> <details className="text-xs text-muted-foreground">
<summary className="cursor-pointer select-none">Evidence</summary> <HelpTip label="The raw evidence text the reviewer captured for this finding">
<summary className="cursor-pointer select-none w-fit">
Evidence
</summary>
</HelpTip>
<pre className="mt-1 whitespace-pre-wrap rounded bg-muted/50 p-2"> <pre className="mt-1 whitespace-pre-wrap rounded bg-muted/50 p-2">
{finding.evidence} {finding.evidence}
</pre> </pre>
@@ -174,20 +178,31 @@ export function TabFindings({ task }: TabFindingsProps) {
{(data?.summary.length ?? 0) > 0 && ( {(data?.summary.length ?? 0) > 0 && (
<div className="flex flex-wrap gap-2"> <div className="flex flex-wrap gap-2">
{data!.summary.map((s) => ( {data!.summary.map((s) => (
<Badge key={s.origin} variant="outline"> <HelpTip
key={s.origin}
label="Closed combines addressed, verified, and waived findings"
>
<Badge variant="outline">
{ORIGIN_LABEL[s.origin] ?? s.origin}: {s.open} open ·{" "} {ORIGIN_LABEL[s.origin] ?? s.origin}: {s.open} open ·{" "}
{s.addressed + s.verified + s.waived} closed {s.addressed + s.verified + s.waived} closed
</Badge> </Badge>
</HelpTip>
))} ))}
</div> </div>
)} )}
{rounds.map((group) => ( {rounds.map((group) => (
<div key={group.round} className="space-y-3"> <div key={group.round} className="space-y-3">
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<h3 className="text-sm font-semibold">Round {group.round}</h3> <HelpTip label="Each bounce back to the assignee starts a new round">
<h3 className="text-sm font-semibold w-fit">
Round {group.round}
</h3>
</HelpTip>
<HelpTip label="Which reviewer stage raised this round's findings">
<Badge variant="outline"> <Badge variant="outline">
{ORIGIN_LABEL[group.origin] ?? group.origin} {ORIGIN_LABEL[group.origin] ?? group.origin}
</Badge> </Badge>
</HelpTip>
</div> </div>
<div className="space-y-3"> <div className="space-y-3">
{group.items.map((f) => ( {group.items.map((f) => (
@@ -62,7 +62,17 @@ function prReviewBadge(task: Task): React.ReactNode {
failed: { label: "Failed", cls: "bg-red-500" }, failed: { label: "Failed", cls: "bg-red-500" },
}; };
const v = map[verdict] ?? { label: verdict, cls: "bg-gray-500" }; const v = map[verdict] ?? { label: verdict, cls: "bg-gray-500" };
return <Badge className={`ml-2 ${v.cls} text-white`}>{v.label}</Badge>; const tip: Record<string, string> = {
approved: "The PR reviewer approved this task's assembled PR.",
passed: "The PR reviewer passed the in-path gate — on to PM review.",
changes_requested: "The PR reviewer asked for changes before merging.",
failed: "The PR reviewer failed this task back to needs_revision.",
};
return (
<HelpTip label={tip[verdict]}>
<Badge className={`ml-2 ${v.cls} text-white`}>{v.label}</Badge>
</HelpTip>
);
} }
// The card background mirrors the PR reviewer's verdict, so a FAILED review reads // The card background mirrors the PR reviewer's verdict, so a FAILED review reads
@@ -269,14 +279,17 @@ function EditableNoteCard({
</TabsTrigger> </TabsTrigger>
</TabsList> </TabsList>
</Tabs> </Tabs>
<HelpTip label="Discard changes without saving">
<Button <Button
size="sm" size="sm"
variant="ghost" variant="ghost"
onClick={handleCancel} onClick={handleCancel}
disabled={updateTask.isPending} disabled={updateTask.isPending}
aria-label="Cancel edit"
> >
<X className="h-4 w-4" /> <X className="h-4 w-4" />
</Button> </Button>
</HelpTip>
<Button <Button
size="sm" size="sm"
onClick={handleSave} onClick={handleSave}
@@ -362,7 +375,11 @@ export function TabNotes({ task }: TabNotesProps) {
task={task} task={task}
field="dev_notes" field="dev_notes"
title="Developer Notes" title="Developer Notes"
icon={<Code className="h-5 w-5" />} icon={
<HelpTip label="Written by the developer — approach, decisions, anything QA should know">
<Code className="h-5 w-5" />
</HelpTip>
}
bgClass="bg-muted/50" bgClass="bg-muted/50"
/> />
@@ -371,7 +388,11 @@ export function TabNotes({ task }: TabNotesProps) {
task={task} task={task}
field="doc_notes" field="doc_notes"
title="Documenter Notes" title="Documenter Notes"
icon={<BookText className="h-5 w-5" />} icon={
<HelpTip label="Written by the documenter during the awaiting_documentation phase">
<BookText className="h-5 w-5" />
</HelpTip>
}
bgClass="bg-muted/50" bgClass="bg-muted/50"
/> />
@@ -382,6 +403,15 @@ export function TabNotes({ task }: TabNotesProps) {
title="QA Notes" title="QA Notes"
icon={<TestTube className="h-5 w-5" />} icon={<TestTube className="h-5 w-5" />}
badge={ badge={
<HelpTip
label={
task.qa_verified === true
? "QA passed this task on to documentation."
: task.qa_verified === false
? "QA failed this task back to needs_revision."
: "QA hasn't reviewed this task yet."
}
>
<Badge <Badge
variant={ variant={
task.qa_verified === true task.qa_verified === true
@@ -398,6 +428,7 @@ export function TabNotes({ task }: TabNotesProps) {
? "Failed" ? "Failed"
: "Pending"} : "Pending"}
</Badge> </Badge>
</HelpTip>
} }
bgClass={ bgClass={
task.qa_verified === true task.qa_verified === true
@@ -54,7 +54,9 @@ export function TabOverview({ task }: TabOverviewProps) {
{task.quick_context && ( {task.quick_context && (
<Card> <Card>
<CardHeader> <CardHeader>
<CardTitle className="text-lg">Quick Context</CardTitle> <HelpTip label="Short notes for resuming work after an interruption — mirrored on the Notes tab">
<CardTitle className="text-lg w-fit">Quick Context</CardTitle>
</HelpTip>
</CardHeader> </CardHeader>
<CardContent> <CardContent>
<Markdown className="text-sm">{task.quick_context}</Markdown> <Markdown className="text-sm">{task.quick_context}</Markdown>
@@ -65,7 +67,9 @@ export function TabOverview({ task }: TabOverviewProps) {
{/* Verification Status */} {/* Verification Status */}
<Card> <Card>
<CardHeader> <CardHeader>
<CardTitle className="text-lg">Verification Status</CardTitle> <HelpTip label="Two independent checks: the developer's self-verification, then QA's separate review">
<CardTitle className="text-lg w-fit">Verification Status</CardTitle>
</HelpTip>
</CardHeader> </CardHeader>
<CardContent> <CardContent>
<div className="flex items-center gap-4"> <div className="flex items-center gap-4">
@@ -73,14 +77,17 @@ export function TabOverview({ task }: TabOverviewProps) {
<span className="text-sm text-muted-foreground"> <span className="text-sm text-muted-foreground">
Self Verified: Self Verified:
</span> </span>
<HelpTip label="The developer confirmed they tested their own work before requesting QA review">
<Badge variant={task.self_verified ? "default" : "secondary"}> <Badge variant={task.self_verified ? "default" : "secondary"}>
{task.self_verified ? "Yes" : "No"} {task.self_verified ? "Yes" : "No"}
</Badge> </Badge>
</HelpTip>
</div> </div>
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<span className="text-sm text-muted-foreground"> <span className="text-sm text-muted-foreground">
QA Verified: QA Verified:
</span> </span>
<HelpTip label="Whether QA reviewed this task: Passed, Failed (back to needs_revision), or Pending (not reviewed yet)">
<Badge <Badge
variant={ variant={
task.qa_verified === true task.qa_verified === true
@@ -96,6 +103,7 @@ export function TabOverview({ task }: TabOverviewProps) {
? "Failed" ? "Failed"
: "Pending"} : "Pending"}
</Badge> </Badge>
</HelpTip>
</div> </div>
</div> </div>
</CardContent> </CardContent>
@@ -126,6 +126,11 @@ function ApproachSection({ task, plan }: { task: Task; plan: TaskPlan }) {
actions={ actions={
isEditing ? ( isEditing ? (
<> <>
{/* Write/Preview is a stateful TabsTrigger — wrapping it in
HelpTip would clobber Radix's data-state (see task-tabs.tsx),
and the two-word label + icon already say what it does, so
it's left untipped rather than reassert-patched for a
same-line write/preview toggle. */}
<Tabs <Tabs
value={editMode} value={editMode}
onValueChange={(v) => setEditMode(v as "write" | "preview")} onValueChange={(v) => setEditMode(v as "write" | "preview")}
@@ -151,6 +156,7 @@ function ApproachSection({ task, plan }: { task: Task; plan: TaskPlan }) {
<X className="h-4 w-4" /> <X className="h-4 w-4" />
</Button> </Button>
</HelpTip> </HelpTip>
<HelpTip label="Save this approach text to the task's plan">
<Button <Button
size="sm" size="sm"
onClick={handleSave} onClick={handleSave}
@@ -160,12 +166,15 @@ function ApproachSection({ task, plan }: { task: Task; plan: TaskPlan }) {
<Check className="h-4 w-4 mr-1" /> <Check className="h-4 w-4 mr-1" />
Save Save
</Button> </Button>
</HelpTip>
</> </>
) : ( ) : (
<HelpTip label="Open the approach text for editing">
<Button size="sm" variant="ghost" onClick={startEditing}> <Button size="sm" variant="ghost" onClick={startEditing}>
<Edit3 className="h-4 w-4 mr-1" /> <Edit3 className="h-4 w-4 mr-1" />
Edit Edit
</Button> </Button>
</HelpTip>
) )
} }
> >
@@ -302,10 +311,12 @@ function SubTasksSection({ task, plan }: { task: Task; plan: TaskPlan }) {
{completedCount}/{subTasks.length} completed {completedCount}/{subTasks.length} completed
</span> </span>
{!isAdding && ( {!isAdding && (
<HelpTip label="Add a new sub-task to this plan">
<Button size="sm" variant="ghost" onClick={() => setIsAdding(true)}> <Button size="sm" variant="ghost" onClick={() => setIsAdding(true)}>
<Plus className="h-4 w-4 mr-1" /> <Plus className="h-4 w-4 mr-1" />
Add Add
</Button> </Button>
</HelpTip>
)} )}
</> </>
} }
@@ -318,11 +329,24 @@ function SubTasksSection({ task, plan }: { task: Task; plan: TaskPlan }) {
key={subtask.id} key={subtask.id}
className="flex items-center gap-3 py-2 group" className="flex items-center gap-3 py-2 group"
> >
<HelpTip
label={
subtask.completed
? "Mark this sub-task incomplete"
: "Mark this sub-task complete"
}
>
{/* Checkbox exposes Radix's own data-state=checked/unchecked
for its check-mark styling; re-assert it explicitly after
the Tooltip wrap so the Slot merge can't clobber it (same
trap as task-tabs.tsx's TabsTrigger). */}
<Checkbox <Checkbox
checked={subtask.completed} checked={subtask.completed}
data-state={subtask.completed ? "checked" : "unchecked"}
onCheckedChange={() => handleToggle(subtask.id)} onCheckedChange={() => handleToggle(subtask.id)}
disabled={updateTask.isPending} disabled={updateTask.isPending}
/> />
</HelpTip>
{editingId === subtask.id ? ( {editingId === subtask.id ? (
<div className="flex-1 flex items-center gap-2"> <div className="flex-1 flex items-center gap-2">
<Input <Input
@@ -339,6 +363,7 @@ function SubTasksSection({ task, plan }: { task: Task; plan: TaskPlan }) {
</div> </div>
) : ( ) : (
<> <>
<HelpTip label="Click to rename this sub-task">
<span <span
className={`flex-1 cursor-pointer hover:bg-muted/30 px-2 py-1 -mx-2 rounded ${ className={`flex-1 cursor-pointer hover:bg-muted/30 px-2 py-1 -mx-2 rounded ${
subtask.completed subtask.completed
@@ -352,11 +377,14 @@ function SubTasksSection({ task, plan }: { task: Task; plan: TaskPlan }) {
> >
{subtask.title} {subtask.title}
{subtask.estimated_hours && ( {subtask.estimated_hours && (
<HelpTip label="Estimated effort for this sub-task, in hours">
<Badge variant="outline" className="ml-2 text-xs"> <Badge variant="outline" className="ml-2 text-xs">
~{subtask.estimated_hours}h ~{subtask.estimated_hours}h
</Badge> </Badge>
</HelpTip>
)} )}
</span> </span>
</HelpTip>
<HelpTip label="Delete this sub-task"> <HelpTip label="Delete this sub-task">
<Button <Button
size="sm" size="sm"
@@ -509,10 +537,12 @@ function TechConsiderationsSection({
} }
actions={ actions={
!isAdding && ( !isAdding && (
<HelpTip label="Add a new technical consideration">
<Button size="sm" variant="ghost" onClick={() => setIsAdding(true)}> <Button size="sm" variant="ghost" onClick={() => setIsAdding(true)}>
<Plus className="h-4 w-4 mr-1" /> <Plus className="h-4 w-4 mr-1" />
Add Add
</Button> </Button>
</HelpTip>
) )
} }
> >
@@ -536,6 +566,7 @@ function TechConsiderationsSection({
</div> </div>
) : ( ) : (
<> <>
<HelpTip label="Click to edit this consideration">
<span <span
className="flex-1 text-sm cursor-pointer hover:bg-muted/30 px-2 py-1 -mx-2 rounded" className="flex-1 text-sm cursor-pointer hover:bg-muted/30 px-2 py-1 -mx-2 rounded"
onClick={() => { onClick={() => {
@@ -545,6 +576,7 @@ function TechConsiderationsSection({
> >
{item} {item}
</span> </span>
</HelpTip>
<HelpTip label="Delete this consideration"> <HelpTip label="Delete this consideration">
<Button <Button
size="sm" size="sm"
@@ -708,10 +740,12 @@ function RisksSection({ task, plan }: { task: Task; plan: TaskPlan }) {
} }
actions={ actions={
!isAdding && ( !isAdding && (
<HelpTip label="Add a new risk with a mitigation and severity">
<Button size="sm" variant="ghost" onClick={() => setIsAdding(true)}> <Button size="sm" variant="ghost" onClick={() => setIsAdding(true)}>
<Plus className="h-4 w-4 mr-1" /> <Plus className="h-4 w-4 mr-1" />
Add Add
</Button> </Button>
</HelpTip>
) )
} }
> >
@@ -733,6 +767,7 @@ function RisksSection({ task, plan }: { task: Task; plan: TaskPlan }) {
className="text-sm" className="text-sm"
/> />
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<HelpTip label="How likely/costly this risk is — see the severity badge for what each level means">
<Select value={editSeverity} onValueChange={setEditSeverity}> <Select value={editSeverity} onValueChange={setEditSeverity}>
<SelectTrigger className="w-32 h-8"> <SelectTrigger className="w-32 h-8">
<SelectValue /> <SelectValue />
@@ -743,6 +778,7 @@ function RisksSection({ task, plan }: { task: Task; plan: TaskPlan }) {
<SelectItem value="high">High</SelectItem> <SelectItem value="high">High</SelectItem>
</SelectContent> </SelectContent>
</Select> </Select>
</HelpTip>
<div className="flex-1" /> <div className="flex-1" />
<HelpTip label="Discard and cancel"> <HelpTip label="Discard and cancel">
<Button <Button
@@ -826,6 +862,7 @@ function RisksSection({ task, plan }: { task: Task; plan: TaskPlan }) {
className="text-sm" className="text-sm"
/> />
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<HelpTip label="How likely/costly this risk is — see the severity badge for what each level means">
<Select value={newSeverity} onValueChange={setNewSeverity}> <Select value={newSeverity} onValueChange={setNewSeverity}>
<SelectTrigger className="w-32 h-8"> <SelectTrigger className="w-32 h-8">
<SelectValue /> <SelectValue />
@@ -836,6 +873,7 @@ function RisksSection({ task, plan }: { task: Task; plan: TaskPlan }) {
<SelectItem value="high">High</SelectItem> <SelectItem value="high">High</SelectItem>
</SelectContent> </SelectContent>
</Select> </Select>
</HelpTip>
<div className="flex-1" /> <div className="flex-1" />
<HelpTip label="Discard and cancel"> <HelpTip label="Discard and cancel">
<Button <Button
@@ -964,10 +1002,12 @@ function OpenQuestionsSection({ task, plan }: { task: Task; plan: TaskPlan }) {
} }
actions={ actions={
!isAdding && ( !isAdding && (
<HelpTip label="Add a new open question">
<Button size="sm" variant="ghost" onClick={() => setIsAdding(true)}> <Button size="sm" variant="ghost" onClick={() => setIsAdding(true)}>
<Plus className="h-4 w-4 mr-1" /> <Plus className="h-4 w-4 mr-1" />
Add Add
</Button> </Button>
</HelpTip>
) )
} }
> >
@@ -1051,12 +1091,14 @@ function OpenQuestionsSection({ task, plan }: { task: Task; plan: TaskPlan }) {
</div> </div>
) : ( ) : (
<div className="ml-7"> <div className="ml-7">
<HelpTip label="No answer recorded yet — click this card to add one">
<Badge <Badge
variant="outline" variant="outline"
className="text-yellow-600 border-yellow-300" className="text-yellow-600 border-yellow-300"
> >
Awaiting Answer Awaiting Answer
</Badge> </Badge>
</HelpTip>
</div> </div>
)} )}
</div> </div>
@@ -1157,10 +1199,12 @@ export function TabPlan({ task }: TabPlanProps) {
A plan will be created once an agent claims and starts working on A plan will be created once an agent claims and starts working on
this task. this task.
</p> </p>
<HelpTip label="Creates an empty plan object on this task so approach/sub-tasks/risks can be filled in manually">
<Button onClick={createPlan} disabled={updateTask.isPending}> <Button onClick={createPlan} disabled={updateTask.isPending}>
<Plus className="h-4 w-4 mr-2" /> <Plus className="h-4 w-4 mr-2" />
Create Plan Create Plan
</Button> </Button>
</HelpTip>
</div> </div>
</CardContent> </CardContent>
</Card> </Card>
@@ -30,6 +30,7 @@ import { toast } from "sonner";
import { getAgentDisplayName } from "@/lib/agent-utils"; import { getAgentDisplayName } from "@/lib/agent-utils";
import { formatAbsoluteTimestamp } from "@/lib/utils"; import { formatAbsoluteTimestamp } from "@/lib/utils";
import { exceedsReadabilityThreshold } from "@/lib/content-readability"; import { exceedsReadabilityThreshold } from "@/lib/content-readability";
import { HelpTip } from "@/components/ui/help-tip";
/** /**
* Only the 2 most recent entries default to expanded; older entries (and * Only the 2 most recent entries default to expanded; older entries (and
@@ -159,12 +160,15 @@ function ProgressUpdatesSection({ task }: { task: Task }) {
<Card> <Card>
<CardHeader className="pb-3"> <CardHeader className="pb-3">
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
<HelpTip label="A timestamped log of status updates entered by devs, QA, or the CEO">
<CardTitle className="text-lg">Progress Updates</CardTitle> <CardTitle className="text-lg">Progress Updates</CardTitle>
</HelpTip>
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<span className="text-sm text-muted-foreground"> <span className="text-sm text-muted-foreground">
{updates.length} update{updates.length !== 1 ? "s" : ""} {updates.length} update{updates.length !== 1 ? "s" : ""}
</span> </span>
{!isAdding && ( {!isAdding && (
<HelpTip label="Log a new progress update, optionally with a completion percentage">
<Button <Button
size="sm" size="sm"
variant="ghost" variant="ghost"
@@ -173,15 +177,18 @@ function ProgressUpdatesSection({ task }: { task: Task }) {
<Plus className="h-4 w-4 mr-1" /> <Plus className="h-4 w-4 mr-1" />
Add Add
</Button> </Button>
</HelpTip>
)} )}
</div> </div>
</div> </div>
{latestWithPercentage && ( {latestWithPercentage && (
<div className="mt-2"> <div className="mt-2">
<HelpTip label="The percentage from the most recent update that included one — manually entered, not computed from acceptance criteria">
<div className="flex items-center justify-between text-sm mb-1"> <div className="flex items-center justify-between text-sm mb-1">
<span className="text-muted-foreground">Overall Progress</span> <span className="text-muted-foreground">Overall Progress</span>
<span className="font-medium">{currentProgress}%</span> <span className="font-medium">{currentProgress}%</span>
</div> </div>
</HelpTip>
<Progress value={currentProgress} className="h-2" /> <Progress value={currentProgress} className="h-2" />
</div> </div>
)} )}
@@ -197,6 +204,7 @@ function ProgressUpdatesSection({ task }: { task: Task }) {
className="text-sm min-h-[80px]" className="text-sm min-h-[80px]"
/> />
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<HelpTip label="Optional — leave blank to log a note with no percentage">
<Input <Input
type="number" type="number"
value={newPercentage} value={newPercentage}
@@ -206,7 +214,9 @@ function ProgressUpdatesSection({ task }: { task: Task }) {
min="0" min="0"
max="100" max="100"
/> />
</HelpTip>
<div className="flex-1" /> <div className="flex-1" />
<HelpTip label="Discard and cancel">
<Button <Button
size="sm" size="sm"
variant="ghost" variant="ghost"
@@ -218,6 +228,8 @@ function ProgressUpdatesSection({ task }: { task: Task }) {
> >
<X className="h-4 w-4" /> <X className="h-4 w-4" />
</Button> </Button>
</HelpTip>
<HelpTip label="Save this progress update to the task">
<Button <Button
size="sm" size="sm"
onClick={handleAdd} onClick={handleAdd}
@@ -226,6 +238,7 @@ function ProgressUpdatesSection({ task }: { task: Task }) {
<Check className="h-4 w-4 mr-1" /> <Check className="h-4 w-4 mr-1" />
Add Update Add Update
</Button> </Button>
</HelpTip>
</div> </div>
</div> </div>
)} )}
@@ -253,9 +266,16 @@ function ProgressUpdatesSection({ task }: { task: Task }) {
className="bg-muted/50 rounded-lg p-3" className="bg-muted/50 rounded-lg p-3"
> >
<div className="flex items-center justify-between mb-1"> <div className="flex items-center justify-between mb-1">
{/* CollapsibleTrigger is a stateful Radix trigger HelpTip's
asChild Slot would clobber its open/closed data-state
(same trap as task-tabs.tsx), and this Collapsible is
uncontrolled so there's no local state to re-assert.
Left untipped; the chevron + agent name are visually
self-explanatory as an expand/collapse row. */}
<CollapsibleTrigger asChild> <CollapsibleTrigger asChild>
<button <button
type="button" type="button"
data-testid="progress-update-trigger"
className="group/trigger flex min-w-0 items-center gap-1 text-left" className="group/trigger flex min-w-0 items-center gap-1 text-left"
> >
<ChevronDown className="h-3 w-3 shrink-0 text-muted-foreground transition-transform duration-200 group-data-[state=closed]/trigger:-rotate-90" /> <ChevronDown className="h-3 w-3 shrink-0 text-muted-foreground transition-transform duration-200 group-data-[state=closed]/trigger:-rotate-90" />
@@ -273,6 +293,7 @@ function ProgressUpdatesSection({ task }: { task: Task }) {
{formatTime(update.timestamp)} ·{" "} {formatTime(update.timestamp)} ·{" "}
{formatAbsoluteTimestamp(update.timestamp)} {formatAbsoluteTimestamp(update.timestamp)}
</span> </span>
<HelpTip label="Delete this progress update">
<Button <Button
size="sm" size="sm"
variant="ghost" variant="ghost"
@@ -281,6 +302,7 @@ function ProgressUpdatesSection({ task }: { task: Task }) {
> >
<Trash2 className="h-3 w-3" /> <Trash2 className="h-3 w-3" />
</Button> </Button>
</HelpTip>
</div> </div>
</div> </div>
<CollapsibleContent> <CollapsibleContent>
@@ -382,15 +404,18 @@ function CheckpointsSection({ task }: { task: Task }) {
<Card> <Card>
<CardHeader className="pb-3"> <CardHeader className="pb-3">
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
<HelpTip label="A saved snapshot of task state + remaining work, useful for handoff between agents">
<CardTitle className="text-lg flex items-center gap-2"> <CardTitle className="text-lg flex items-center gap-2">
<Bookmark className="h-5 w-5" /> <Bookmark className="h-5 w-5" />
Checkpoints Checkpoints
</CardTitle> </CardTitle>
</HelpTip>
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<span className="text-sm text-muted-foreground"> <span className="text-sm text-muted-foreground">
{checkpoints.length} saved {checkpoints.length} saved
</span> </span>
{!isAdding && ( {!isAdding && (
<HelpTip label="Save a new checkpoint of the current state and remaining work">
<Button <Button
size="sm" size="sm"
variant="ghost" variant="ghost"
@@ -399,6 +424,7 @@ function CheckpointsSection({ task }: { task: Task }) {
<Plus className="h-4 w-4 mr-1" /> <Plus className="h-4 w-4 mr-1" />
Add Add
</Button> </Button>
</HelpTip>
)} )}
</div> </div>
</div> </div>
@@ -442,6 +468,7 @@ function CheckpointsSection({ task }: { task: Task }) {
</div> </div>
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<div className="flex-1" /> <div className="flex-1" />
<HelpTip label="Discard and cancel">
<Button <Button
size="sm" size="sm"
variant="ghost" variant="ghost"
@@ -454,6 +481,8 @@ function CheckpointsSection({ task }: { task: Task }) {
> >
<X className="h-4 w-4" /> <X className="h-4 w-4" />
</Button> </Button>
</HelpTip>
<HelpTip label="Save this checkpoint to the task">
<Button <Button
size="sm" size="sm"
onClick={handleAdd} onClick={handleAdd}
@@ -462,6 +491,7 @@ function CheckpointsSection({ task }: { task: Task }) {
<Check className="h-4 w-4 mr-1" /> <Check className="h-4 w-4 mr-1" />
Save Checkpoint Save Checkpoint
</Button> </Button>
</HelpTip>
</div> </div>
</div> </div>
)} )}
@@ -491,6 +521,8 @@ function CheckpointsSection({ task }: { task: Task }) {
defaultOpen={defaultEntryOpen(idx, checkpointContent)} defaultOpen={defaultEntryOpen(idx, checkpointContent)}
> >
<div className="bg-primary/10 px-4 py-2 flex items-center justify-between"> <div className="bg-primary/10 px-4 py-2 flex items-center justify-between">
{/* Uncontrolled CollapsibleTrigger left untipped, see
the identical note in ProgressUpdatesSection above. */}
<CollapsibleTrigger asChild> <CollapsibleTrigger asChild>
<button <button
type="button" type="button"
@@ -512,6 +544,7 @@ function CheckpointsSection({ task }: { task: Task }) {
{formatTime(checkpoint.timestamp)} ·{" "} {formatTime(checkpoint.timestamp)} ·{" "}
{formatAbsoluteTimestamp(checkpoint.timestamp)} {formatAbsoluteTimestamp(checkpoint.timestamp)}
</span> </span>
<HelpTip label="Delete this checkpoint">
<Button <Button
size="sm" size="sm"
variant="ghost" variant="ghost"
@@ -520,6 +553,7 @@ function CheckpointsSection({ task }: { task: Task }) {
> >
<Trash2 className="h-3 w-3" /> <Trash2 className="h-3 w-3" />
</Button> </Button>
</HelpTip>
</div> </div>
</div> </div>
<CollapsibleContent> <CollapsibleContent>
@@ -61,7 +61,11 @@ export function EscalateToCeoDialog({
</DialogHeader> </DialogHeader>
<div className="grid gap-4 py-4"> <div className="grid gap-4 py-4">
<div className="grid gap-2"> <div className="grid gap-2">
<Label htmlFor="reason">Reason for escalation</Label> <HelpTip label="Shown to the CEO in their approval queue alongside this task">
<Label htmlFor="reason" className="w-fit">
Reason for escalation
</Label>
</HelpTip>
<Textarea <Textarea
id="reason" id="reason"
value={reason} value={reason}
@@ -72,15 +76,24 @@ export function EscalateToCeoDialog({
</div> </div>
</div> </div>
<DialogFooter> <DialogFooter>
<HelpTip label="Closes without escalating; the task stays in its current status">
<Button variant="outline" onClick={() => handleOpenChange(false)}> <Button variant="outline" onClick={() => handleOpenChange(false)}>
Cancel Cancel
</Button> </Button>
</HelpTip>
<HelpTip label={!reason.trim() ? "Enter a reason to enable" : null}>
<span
className="inline-block"
tabIndex={!reason.trim() ? 0 : undefined}
>
<Button <Button
onClick={handleConfirm} onClick={handleConfirm}
disabled={!reason.trim() || isPending} disabled={!reason.trim() || isPending}
> >
{isPending ? "Escalating..." : "Escalate"} {isPending ? "Escalating..." : "Escalate"}
</Button> </Button>
</span>
</HelpTip>
</DialogFooter> </DialogFooter>
</DialogContent> </DialogContent>
</Dialog> </Dialog>
@@ -126,7 +139,11 @@ export function CeoRejectDialog({
</DialogHeader> </DialogHeader>
<div className="grid gap-4 py-4"> <div className="grid gap-4 py-4">
<div className="grid gap-2"> <div className="grid gap-2">
<Label htmlFor="notes">Required changes</Label> <HelpTip label="Recorded as the CEO's rejection reason; the task returns to needs_revision">
<Label htmlFor="notes" className="w-fit">
Required changes
</Label>
</HelpTip>
<Textarea <Textarea
id="notes" id="notes"
value={notes} value={notes}
@@ -137,9 +154,16 @@ export function CeoRejectDialog({
</div> </div>
</div> </div>
<DialogFooter> <DialogFooter>
<HelpTip label="Closes without rejecting; the task stays in awaiting CEO approval">
<Button variant="outline" onClick={() => handleOpenChange(false)}> <Button variant="outline" onClick={() => handleOpenChange(false)}>
Cancel Cancel
</Button> </Button>
</HelpTip>
<HelpTip label={!notes.trim() ? "Explain what changes are needed to enable" : null}>
<span
className="inline-block"
tabIndex={!notes.trim() ? 0 : undefined}
>
<Button <Button
variant="destructive" variant="destructive"
onClick={handleConfirm} onClick={handleConfirm}
@@ -147,6 +171,8 @@ export function CeoRejectDialog({
> >
{isPending ? "Submitting..." : "Request Changes"} {isPending ? "Submitting..." : "Request Changes"}
</Button> </Button>
</span>
</HelpTip>
</DialogFooter> </DialogFooter>
</DialogContent> </DialogContent>
</Dialog> </Dialog>
@@ -179,6 +205,7 @@ export function ApproveAndMergeDialog({
</DialogDescription> </DialogDescription>
</DialogHeader> </DialogHeader>
<DialogFooter> <DialogFooter>
<HelpTip label="Closes without approving or merging">
<Button <Button
variant="outline" variant="outline"
onClick={() => onOpenChange(false)} onClick={() => onOpenChange(false)}
@@ -186,6 +213,7 @@ export function ApproveAndMergeDialog({
> >
Cancel Cancel
</Button> </Button>
</HelpTip>
<Button onClick={onConfirm} disabled={isPending}> <Button onClick={onConfirm} disabled={isPending}>
{isPending ? "Merging..." : "Approve & Merge"} {isPending ? "Merging..." : "Approve & Merge"}
</Button> </Button>
@@ -263,13 +291,19 @@ export function CeoApproveDialog({
</div> </div>
</div> </div>
<DialogFooter> <DialogFooter>
<HelpTip label="Closes without approving; the task stays in awaiting CEO approval">
<Button variant="outline" onClick={() => handleOpenChange(false)}> <Button variant="outline" onClick={() => handleOpenChange(false)}>
Cancel Cancel
</Button> </Button>
</HelpTip>
{/* Button's own disabled:pointer-events-none would swallow hover, so
the tip sits on a wrapping span rather than the Button itself. */}
<HelpTip label={remainingCharsTip(notes.trim().length, _CEO_NOTES_MIN)}> <HelpTip label={remainingCharsTip(notes.trim().length, _CEO_NOTES_MIN)}>
<span className="inline-block" tabIndex={tooShort ? 0 : undefined}>
<Button onClick={handleConfirm} disabled={tooShort || isPending}> <Button onClick={handleConfirm} disabled={tooShort || isPending}>
{isPending ? "Approving..." : "Approve & Merge"} {isPending ? "Approving..." : "Approve & Merge"}
</Button> </Button>
</span>
</HelpTip> </HelpTip>
</DialogFooter> </DialogFooter>
</DialogContent> </DialogContent>
@@ -344,10 +378,15 @@ export function RequiredNotesDialog({
</div> </div>
</div> </div>
<DialogFooter> <DialogFooter>
<HelpTip label="Closes without confirming; nothing is changed">
<Button variant="outline" onClick={() => handleOpenChange(false)}> <Button variant="outline" onClick={() => handleOpenChange(false)}>
Cancel Cancel
</Button> </Button>
</HelpTip>
{/* Button's own disabled:pointer-events-none would swallow hover, so
the tip sits on a wrapping span rather than the Button itself. */}
<HelpTip label={remainingCharsTip(text.trim().length, minChars)}> <HelpTip label={remainingCharsTip(text.trim().length, minChars)}>
<span className="inline-block" tabIndex={tooShort ? 0 : undefined}>
<Button <Button
variant={destructive ? "destructive" : "default"} variant={destructive ? "destructive" : "default"}
onClick={handleConfirm} onClick={handleConfirm}
@@ -355,6 +394,7 @@ export function RequiredNotesDialog({
> >
{isPending ? "Working..." : confirmLabel} {isPending ? "Working..." : confirmLabel}
</Button> </Button>
</span>
</HelpTip> </HelpTip>
</DialogFooter> </DialogFooter>
</DialogContent> </DialogContent>
@@ -401,7 +441,11 @@ export function CreateBranchDialog({
</DialogHeader> </DialogHeader>
<div className="grid gap-4 py-4"> <div className="grid gap-4 py-4">
<div className="grid gap-2"> <div className="grid gap-2">
<Label htmlFor="branch-type">Branch Type</Label> <HelpTip label="Chooses the branch-name prefix — see the naming preview below">
<Label htmlFor="branch-type" className="w-fit">
Branch Type
</Label>
</HelpTip>
<Select value={branchType} onValueChange={setBranchType}> <Select value={branchType} onValueChange={setBranchType}>
<SelectTrigger> <SelectTrigger>
<SelectValue /> <SelectValue />
@@ -420,12 +464,16 @@ export function CreateBranchDialog({
</div> </div>
</div> </div>
<DialogFooter> <DialogFooter>
<HelpTip label="Closes without creating a branch">
<Button variant="outline" onClick={() => handleOpenChange(false)}> <Button variant="outline" onClick={() => handleOpenChange(false)}>
Cancel Cancel
</Button> </Button>
</HelpTip>
<HelpTip label="Creates and checks out the branch immediately — no confirmation after this">
<Button onClick={handleConfirm} disabled={isPending}> <Button onClick={handleConfirm} disabled={isPending}>
{isPending ? "Creating..." : "Create Branch"} {isPending ? "Creating..." : "Create Branch"}
</Button> </Button>
</HelpTip>
</DialogFooter> </DialogFooter>
</DialogContent> </DialogContent>
</Dialog> </Dialog>
@@ -482,7 +530,11 @@ export function CreatePRDialog({
</DialogHeader> </DialogHeader>
<div className="grid gap-4 py-4"> <div className="grid gap-4 py-4">
<div className="grid gap-2"> <div className="grid gap-2">
<Label htmlFor="pr-title">Title</Label> <HelpTip label="Seeded from the task title by default — edit before creating">
<Label htmlFor="pr-title" className="w-fit">
Title
</Label>
</HelpTip>
<Input <Input
id="pr-title" id="pr-title"
value={title} value={title}
@@ -491,7 +543,11 @@ export function CreatePRDialog({
/> />
</div> </div>
<div className="grid gap-2"> <div className="grid gap-2">
<Label htmlFor="pr-body">Description</Label> <HelpTip label="Becomes the pull request's description on GitHub">
<Label htmlFor="pr-body" className="w-fit">
Description
</Label>
</HelpTip>
<Textarea <Textarea
id="pr-body" id="pr-body"
value={body} value={body}
@@ -502,12 +558,24 @@ export function CreatePRDialog({
</div> </div>
</div> </div>
<DialogFooter> <DialogFooter>
<HelpTip label="Closes without creating a PR">
<Button variant="outline" onClick={() => handleOpenChange(false)}> <Button variant="outline" onClick={() => handleOpenChange(false)}>
Cancel Cancel
</Button> </Button>
<Button onClick={handleConfirm} disabled={!title.trim() || isPending}> </HelpTip>
<HelpTip label={!title.trim() ? "Enter a title to enable" : null}>
<span
className="inline-block"
tabIndex={!title.trim() ? 0 : undefined}
>
<Button
onClick={handleConfirm}
disabled={!title.trim() || isPending}
>
{isPending ? "Creating..." : "Create PR"} {isPending ? "Creating..." : "Create PR"}
</Button> </Button>
</span>
</HelpTip>
</DialogFooter> </DialogFooter>
</DialogContent> </DialogContent>
</Dialog> </Dialog>
@@ -3,6 +3,7 @@
import { Task } from "@/types"; import { Task } from "@/types";
import { useTask } from "@/hooks/use-tasks"; import { useTask } from "@/hooks/use-tasks";
import { Skeleton } from "@/components/ui/skeleton"; import { Skeleton } from "@/components/ui/skeleton";
import { HelpTip } from "@/components/ui/help-tip";
import { ChevronRight } from "lucide-react"; import { ChevronRight } from "lucide-react";
import Link from "next/link"; import Link from "next/link";
@@ -28,6 +29,7 @@ export function TaskBreadcrumb({ task }: TaskBreadcrumbProps) {
{isLoading || !parent ? ( {isLoading || !parent ? (
<Skeleton className="h-4 w-32" /> <Skeleton className="h-4 w-32" />
) : ( ) : (
<HelpTip label="Go to the parent task — only the immediate parent is shown here, not the full ancestry chain">
<Link <Link
href={`/tasks/${parent.id}`} href={`/tasks/${parent.id}`}
prefetch={false} prefetch={false}
@@ -36,6 +38,7 @@ export function TaskBreadcrumb({ task }: TaskBreadcrumbProps) {
> >
{parent.title} {parent.title}
</Link> </Link>
</HelpTip>
)} )}
<ChevronRight className="h-3.5 w-3.5 shrink-0" /> <ChevronRight className="h-3.5 w-3.5 shrink-0" />
<span className="truncate text-foreground/70" title={task.title}> <span className="truncate text-foreground/70" title={task.title}>
@@ -189,7 +189,9 @@ export function TaskDescription({ task }: TaskDescriptionProps) {
defaultOpen={false} defaultOpen={false}
title={ title={
<> <>
<HelpTip label="Read-only architectural rules derived from the project's conventions — auto-attached, not editable here">
<ShieldAlert className="h-4 w-4 text-amber-600 dark:text-amber-400" /> <ShieldAlert className="h-4 w-4 text-amber-600 dark:text-amber-400" />
</HelpTip>
<span className="text-amber-800 dark:text-amber-300"> <span className="text-amber-800 dark:text-amber-300">
Constraints Constraints
</span> </span>
@@ -56,6 +56,7 @@ import {
import { toast } from "sonner"; import { toast } from "sonner";
import { TaskTypeBadge } from "../task-type-badge"; import { TaskTypeBadge } from "../task-type-badge";
import { CopyButton } from "@/components/ui/copy-button"; import { CopyButton } from "@/components/ui/copy-button";
import { HelpTip } from "@/components/ui/help-tip";
import { import {
Tooltip, Tooltip,
TooltipContent, TooltipContent,
@@ -117,6 +118,55 @@ const statusLabels: Record<TaskStatus, string> = {
[TaskStatus.CANCELLED]: "Cancelled", [TaskStatus.CANCELLED]: "Cancelled",
}; };
// Per-action explanation of the real backend consequence — grounded in
// roboco/foundation/policy/lifecycle.py (ActionSpec/IntentSpec source +
// target statuses, allowed_roles) and the handlers in
// app/(dashboard)/tasks/[taskId]/page.tsx (handleAction). Some of these
// route through an admin-override PATCH rather than a dedicated lifecycle
// verb — the tip says so where that's the case.
const ACTION_TIPS: Record<string, string> = {
claim: "Locks this task to you so no one else can claim it (pending → claimed).",
start: "Begins active work on the task (claimed → in_progress).",
"create-branch":
"Creates the git branch for this task; status does not change.",
pause:
"Pauses active work (in_progress → paused); use Resume to continue later.",
block:
"Flags an external blocker (in_progress → blocked); only a PM can unblock it.",
"create-pr":
"Opens the pull request for this task's branch; status does not change.",
verify: "Marks work self-verified, the step before QA (in_progress → verifying).",
unblock: "PM-only: clears the blocker and resumes work (blocked → in_progress).",
resume: "Resumes paused work (paused → in_progress).",
"submit-qa": "Hands the task to QA for review (verifying → awaiting_qa).",
"pass-qa":
"QA-only: passes review and starts docs (awaiting_qa → awaiting_documentation).",
"fail-qa":
"QA-only: sends the task back to the developer (awaiting_qa → needs_revision).",
"docs-complete":
"Documenter-only: marks docs done, moves to PM review (→ awaiting_pm_review).",
"submit-pm-review":
"Sends the finished work to the PM for merge review (→ awaiting_pm_review).",
complete:
"PM-only: approves and marks the task completed (awaiting_pm_review → completed).",
"escalate-to-ceo":
"Sends this task to the CEO for final sign-off (→ awaiting_ceo_approval).",
"request-changes":
"Sends the task back to the developer for changes (→ needs_revision).",
"ceo-approve":
"CEO-only: approves and completes the task (→ completed); requires a note.",
"approve-and-merge":
"CEO-only: merges the PR and marks the task completed in one step.",
"ceo-reject": "CEO-only: sends the task back for revision (→ needs_revision).",
reopen: "Admin override: force-sets a cancelled task back to pending.",
activate: "PM-only: releases the task from backlog into the claim pool (→ pending).",
"start-revision":
"Admin override: forces the task straight to in_progress, skipping re-claim.",
cancel: "PM/CEO-only: cancels the task permanently; requires a documented reason.",
"merge-pr":
"Merges this task's PR on GitHub; the backend updates task state as a side effect.",
};
interface TaskHeaderProps { interface TaskHeaderProps {
task: Task; task: Task;
onAction?: (action: string) => void; onAction?: (action: string) => void;
@@ -515,13 +565,14 @@ export function TaskHeader({ task, onAction, nav }: TaskHeaderProps) {
disabled={updateTask.isPending} disabled={updateTask.isPending}
/> />
) : ( ) : (
<HelpTip label={`Click to rename — full title: "${task.title}"`}>
<h1 <h1
className="text-2xl font-bold cursor-pointer hover:bg-muted/50 px-2 py-1 -mx-2 rounded transition-colors truncate" className="text-2xl font-bold cursor-pointer hover:bg-muted/50 px-2 py-1 -mx-2 rounded transition-colors truncate"
onClick={startEditingTitle} onClick={startEditingTitle}
title={task.title}
> >
{task.title} {task.title}
</h1> </h1>
</HelpTip>
)} )}
{/* Row 2: copyable task id + status + team + type. The dropdowns are {/* Row 2: copyable task id + status + team + type. The dropdowns are
@@ -530,13 +581,12 @@ export function TaskHeader({ task, onAction, nav }: TaskHeaderProps) {
flex-wrap: on narrow viewports the id/status/team/type group flex-wrap: on narrow viewports the id/status/team/type group
wraps to further rows instead of overflowing horizontally. */} wraps to further rows instead of overflowing horizontally. */}
<div className="flex flex-wrap items-center gap-2 mt-1.5"> <div className="flex flex-wrap items-center gap-2 mt-1.5">
<span <HelpTip label={`Full task ID: ${task.id}`}>
className="inline-flex h-7 shrink-0 items-center gap-1 rounded-md border bg-muted/40 px-2.5 font-mono text-sm text-muted-foreground" <span className="inline-flex h-7 shrink-0 items-center gap-1 rounded-md border bg-muted/40 px-2.5 font-mono text-sm text-muted-foreground">
title={task.id}
>
#{task.id.slice(0, 8)} #{task.id.slice(0, 8)}
<CopyButton value={task.id} className="-mr-1 px-1 py-0" /> <CopyButton value={task.id} className="-mr-1 px-1 py-0" />
</span> </span>
</HelpTip>
<span className="text-muted-foreground shrink-0">|</span> <span className="text-muted-foreground shrink-0">|</span>
@@ -669,23 +719,27 @@ export function TaskHeader({ task, onAction, nav }: TaskHeaderProps) {
</TooltipContent> </TooltipContent>
</Tooltip> </Tooltip>
<DropdownMenuContent align="end"> <DropdownMenuContent align="end">
{/* Lifecycle actions (non-cancel) */} {/* Lifecycle actions (non-cancel) each tip states the real
sourcetarget status transition and which role can fire
it, grounded in lifecycle.py's ActionSpec/IntentSpec table. */}
{actions {actions
.filter((a) => a.action !== "cancel") .filter((a) => a.action !== "cancel")
.map((action) => ( .map((action) => (
<HelpTip key={action.action} label={ACTION_TIPS[action.action]}>
<DropdownMenuItem <DropdownMenuItem
key={action.action}
onClick={() => handleAction(action.action)} onClick={() => handleAction(action.action)}
> >
{action.icon} {action.icon}
{action.label} {action.label}
</DropdownMenuItem> </DropdownMenuItem>
</HelpTip>
))} ))}
{/* Cancel action */} {/* Cancel action */}
{actions.some((a) => a.action === "cancel") && ( {actions.some((a) => a.action === "cancel") && (
<> <>
<DropdownMenuSeparator /> <DropdownMenuSeparator />
<HelpTip label={ACTION_TIPS.cancel}>
<DropdownMenuItem <DropdownMenuItem
onClick={() => handleAction("cancel")} onClick={() => handleAction("cancel")}
className="text-orange-600" className="text-orange-600"
@@ -693,11 +747,13 @@ export function TaskHeader({ task, onAction, nav }: TaskHeaderProps) {
<XCircle className="h-4 w-4 mr-2" /> <XCircle className="h-4 w-4 mr-2" />
Cancel Task Cancel Task
</DropdownMenuItem> </DropdownMenuItem>
</HelpTip>
</> </>
)} )}
{/* Delete option */} {/* Delete option */}
<DropdownMenuSeparator /> <DropdownMenuSeparator />
<HelpTip label="Permanently deletes this task record from the database; cannot be undone.">
<DropdownMenuItem <DropdownMenuItem
onClick={() => setDeleteOpen(true)} onClick={() => setDeleteOpen(true)}
className="text-red-600" className="text-red-600"
@@ -705,6 +761,7 @@ export function TaskHeader({ task, onAction, nav }: TaskHeaderProps) {
<Trash2 className="h-4 w-4 mr-2" /> <Trash2 className="h-4 w-4 mr-2" />
Delete Task Delete Task
</DropdownMenuItem> </DropdownMenuItem>
</HelpTip>
</DropdownMenuContent> </DropdownMenuContent>
</DropdownMenu> </DropdownMenu>
)} )}
@@ -31,6 +31,7 @@ import {
import { toast } from "sonner"; import { toast } from "sonner";
import { getAgentDisplayName, resolveToSlug } from "@/lib/agent-utils"; import { getAgentDisplayName, resolveToSlug } from "@/lib/agent-utils";
import { branchUrl } from "@/lib/repo-url"; import { branchUrl } from "@/lib/repo-url";
import { formatAbsoluteTimestamp } from "@/lib/utils";
import { CopyButton } from "@/components/ui/copy-button"; import { CopyButton } from "@/components/ui/copy-button";
import { HelpTip } from "@/components/ui/help-tip"; import { HelpTip } from "@/components/ui/help-tip";
import { TaskTypeBadge } from "../task-type-badge"; import { TaskTypeBadge } from "../task-type-badge";
@@ -232,6 +233,7 @@ export function TaskMetadata({ task }: TaskMetadataProps) {
<AlertTriangle className="h-4 w-4" /> <AlertTriangle className="h-4 w-4" />
Priority Priority
</div> </div>
<HelpTip label="How urgently this should be worked — P0 is highest, P3 is lowest">
<Select <Select
value={task.priority.toString()} value={task.priority.toString()}
onValueChange={handlePriorityChange} onValueChange={handlePriorityChange}
@@ -254,6 +256,7 @@ export function TaskMetadata({ task }: TaskMetadataProps) {
))} ))}
</SelectContent> </SelectContent>
</Select> </Select>
</HelpTip>
</CardContent> </CardContent>
</Card> </Card>
@@ -264,6 +267,7 @@ export function TaskMetadata({ task }: TaskMetadataProps) {
<Target className="h-4 w-4" /> <Target className="h-4 w-4" />
Complexity Complexity
</div> </div>
<HelpTip label="A manual estimate of implementation effort; not derived from the plan or ACs">
<Select <Select
value={task.estimated_complexity} value={task.estimated_complexity}
onValueChange={handleComplexityChange} onValueChange={handleComplexityChange}
@@ -280,6 +284,7 @@ export function TaskMetadata({ task }: TaskMetadataProps) {
))} ))}
</SelectContent> </SelectContent>
</Select> </Select>
</HelpTip>
</CardContent> </CardContent>
</Card> </Card>
@@ -302,13 +307,14 @@ export function TaskMetadata({ task }: TaskMetadataProps) {
disabled={updateTask.isPending} disabled={updateTask.isPending}
/> />
) : ( ) : (
<HelpTip label="Click to reassign — enter an agent slug (e.g. be-dev-1), or clear to unassign">
<span <span
className="font-medium cursor-pointer hover:bg-muted/50 px-2 py-1 -mx-2 rounded transition-colors inline-block" className="font-medium cursor-pointer hover:bg-muted/50 px-2 py-1 -mx-2 rounded transition-colors inline-block"
onClick={startEditingAssigned} onClick={startEditingAssigned}
title="Click to edit"
> >
{getAgentDisplayName(task.assigned_to)} {getAgentDisplayName(task.assigned_to)}
</span> </span>
</HelpTip>
)} )}
</CardContent> </CardContent>
</Card> </Card>
@@ -320,9 +326,11 @@ export function TaskMetadata({ task }: TaskMetadataProps) {
<User className="h-4 w-4" /> <User className="h-4 w-4" />
Created By Created By
</div> </div>
<HelpTip label="The agent or human who originally created this task record">
<span className="font-medium"> <span className="font-medium">
{getAgentDisplayName(task.created_by)} {getAgentDisplayName(task.created_by)}
</span> </span>
</HelpTip>
</CardContent> </CardContent>
</Card> </Card>
@@ -348,9 +356,17 @@ export function TaskMetadata({ task }: TaskMetadataProps) {
<Calendar className="h-4 w-4" /> <Calendar className="h-4 w-4" />
Created Created
</div> </div>
<HelpTip
label={
task.created_at
? `Exact timestamp: ${formatAbsoluteTimestamp(task.created_at)}`
: ""
}
>
<span className="font-medium"> <span className="font-medium">
{formatRelativeTime(task.created_at)} {formatRelativeTime(task.created_at)}
</span> </span>
</HelpTip>
</CardContent> </CardContent>
</Card> </Card>
@@ -361,9 +377,17 @@ export function TaskMetadata({ task }: TaskMetadataProps) {
<Clock className="h-4 w-4" /> <Clock className="h-4 w-4" />
Started Started
</div> </div>
<HelpTip
label={
task.started_at
? `Exact timestamp: ${formatAbsoluteTimestamp(task.started_at)}`
: ""
}
>
<span className="font-medium"> <span className="font-medium">
{formatRelativeTime(task.started_at)} {formatRelativeTime(task.started_at)}
</span> </span>
</HelpTip>
</CardContent> </CardContent>
</Card> </Card>
@@ -386,13 +410,14 @@ export function TaskMetadata({ task }: TaskMetadataProps) {
disabled={updateTask.isPending} disabled={updateTask.isPending}
/> />
) : ( ) : (
<HelpTip label="Click to set/change the target completion date">
<span <span
className="font-medium cursor-pointer hover:bg-muted/50 px-2 py-1 -mx-2 rounded transition-colors inline-block" className="font-medium cursor-pointer hover:bg-muted/50 px-2 py-1 -mx-2 rounded transition-colors inline-block"
onClick={startEditingTargetDate} onClick={startEditingTargetDate}
title="Click to edit"
> >
{task.target_date ? formatDate(task.target_date) : "Not set"} {task.target_date ? formatDate(task.target_date) : "Not set"}
</span> </span>
</HelpTip>
)} )}
</CardContent> </CardContent>
</Card> </Card>
@@ -404,9 +429,17 @@ export function TaskMetadata({ task }: TaskMetadataProps) {
<Clock className="h-4 w-4" /> <Clock className="h-4 w-4" />
Completed Completed
</div> </div>
<HelpTip
label={
task.completed_at
? `Exact timestamp: ${formatAbsoluteTimestamp(task.completed_at)}`
: ""
}
>
<span className="font-medium"> <span className="font-medium">
{formatRelativeTime(task.completed_at)} {formatRelativeTime(task.completed_at)}
</span> </span>
</HelpTip>
</CardContent> </CardContent>
</Card> </Card>
@@ -462,21 +495,24 @@ export function TaskMetadata({ task }: TaskMetadataProps) {
</div> </div>
<div className="flex items-center gap-1"> <div className="flex items-center gap-1">
{branchHref ? ( {branchHref ? (
<HelpTip label="Open this branch on GitHub in a new tab">
<a <a
href={branchHref} href={branchHref}
target="_blank" target="_blank"
rel="noopener noreferrer" rel="noopener noreferrer"
title="Open branch in GitHub"
className="inline-flex transition-opacity hover:opacity-80" className="inline-flex transition-opacity hover:opacity-80"
> >
<Badge variant="outline" className="font-mono text-sm"> <Badge variant="outline" className="font-mono text-sm">
{task.branch_name} {task.branch_name}
</Badge> </Badge>
</a> </a>
</HelpTip>
) : ( ) : (
<HelpTip label="The git branch for this task; no project git_url configured to link out to it">
<Badge variant="outline" className="font-mono text-sm"> <Badge variant="outline" className="font-mono text-sm">
{task.branch_name} {task.branch_name}
</Badge> </Badge>
</HelpTip>
)} )}
<CopyButton value={task.branch_name} /> <CopyButton value={task.branch_name} />
</div> </div>
@@ -493,6 +529,7 @@ export function TaskMetadata({ task }: TaskMetadataProps) {
Pull Request Pull Request
</div> </div>
{task.pr_url ? ( {task.pr_url ? (
<HelpTip label="Opens this task's pull request on GitHub in a new tab">
<a <a
href={task.pr_url} href={task.pr_url}
target="_blank" target="_blank"
@@ -502,8 +539,11 @@ export function TaskMetadata({ task }: TaskMetadataProps) {
PR #{task.pr_number} PR #{task.pr_number}
<ExternalLink className="h-3 w-3" /> <ExternalLink className="h-3 w-3" />
</a> </a>
</HelpTip>
) : ( ) : (
<HelpTip label="PR number on record; no pr_url stored to link out to it">
<span className="font-medium">#{task.pr_number}</span> <span className="font-medium">#{task.pr_number}</span>
</HelpTip>
)} )}
</CardContent> </CardContent>
</Card> </Card>
@@ -517,6 +557,7 @@ export function TaskMetadata({ task }: TaskMetadataProps) {
Project Project
</div> </div>
{task.project_id && project ? ( {task.project_id && project ? (
<HelpTip label="Opens the Projects list (not a deep link to this specific project)">
<Link <Link
prefetch={false} prefetch={false}
href={`/projects`} href={`/projects`}
@@ -524,8 +565,11 @@ export function TaskMetadata({ task }: TaskMetadataProps) {
> >
{project.name} {project.name}
</Link> </Link>
</HelpTip>
) : ( ) : (
<HelpTip label="This task has no associated project">
<span className="font-medium text-muted-foreground">-</span> <span className="font-medium text-muted-foreground">-</span>
</HelpTip>
)} )}
</CardContent> </CardContent>
</Card> </Card>
@@ -191,7 +191,8 @@ export function WorkSessionCard({ taskId }: WorkSessionCardProps) {
{session.branch_name} {session.branch_name}
</code> </code>
</div> </div>
<div className="flex items-center gap-2 text-sm text-muted-foreground"> <HelpTip label="Base is the branch this session forked from; target is where its PR merges into">
<div className="flex items-center gap-2 text-sm text-muted-foreground w-fit">
<span>Base:</span> <span>Base:</span>
<code className="bg-muted px-1.5 py-0.5 rounded text-xs font-mono"> <code className="bg-muted px-1.5 py-0.5 rounded text-xs font-mono">
{session.base_branch} {session.base_branch}
@@ -201,6 +202,7 @@ export function WorkSessionCard({ taskId }: WorkSessionCardProps) {
{session.target_branch} {session.target_branch}
</code> </code>
</div> </div>
</HelpTip>
</div> </div>
{/* PR Info */} {/* PR Info */}
@@ -226,6 +228,7 @@ export function WorkSessionCard({ taskId }: WorkSessionCardProps) {
</div> </div>
</div> </div>
{session.pr_url && ( {session.pr_url && (
<HelpTip label="Opens the pull request on GitHub in a new tab">
<Button variant="outline" size="sm" asChild> <Button variant="outline" size="sm" asChild>
<a <a
href={session.pr_url} href={session.pr_url}
@@ -236,26 +239,31 @@ export function WorkSessionCard({ taskId }: WorkSessionCardProps) {
View PR View PR
</a> </a>
</Button> </Button>
</HelpTip>
)} )}
</div> </div>
)} )}
{/* Stats Row */} {/* Stats Row */}
<div className="flex items-center gap-4 pt-2"> <div className="flex items-center gap-4 pt-2">
<div className="flex items-center gap-2"> <HelpTip label="Commits recorded against this branch's work session">
<div className="flex items-center gap-2 w-fit">
<GitCommit className="h-4 w-4 text-muted-foreground" /> <GitCommit className="h-4 w-4 text-muted-foreground" />
<span className="text-sm"> <span className="text-sm">
{session.commits.length} commit {session.commits.length} commit
{session.commits.length !== 1 ? "s" : ""} {session.commits.length !== 1 ? "s" : ""}
</span> </span>
</div> </div>
<div className="flex items-center gap-2"> </HelpTip>
<HelpTip label="Distinct files touched across this session's commits">
<div className="flex items-center gap-2 w-fit">
<FileCode className="h-4 w-4 text-muted-foreground" /> <FileCode className="h-4 w-4 text-muted-foreground" />
<span className="text-sm"> <span className="text-sm">
{session.files_modified.length} file {session.files_modified.length} file
{session.files_modified.length !== 1 ? "s" : ""} {session.files_modified.length !== 1 ? "s" : ""}
</span> </span>
</div> </div>
</HelpTip>
<HelpTip label={formatAbsoluteTimestamp(session.started_at)}> <HelpTip label={formatAbsoluteTimestamp(session.started_at)}>
<div className="text-sm text-muted-foreground ml-auto w-fit"> <div className="text-sm text-muted-foreground ml-auto w-fit">
Started{" "} Started{" "}
@@ -268,12 +276,14 @@ export function WorkSessionCard({ taskId }: WorkSessionCardProps) {
{/* View Full Session Link */} {/* View Full Session Link */}
<div className="flex justify-end pt-2"> <div className="flex justify-end pt-2">
<HelpTip label="Opens the full work-session record — branch, every commit, and status history">
<Link href={`/work-sessions/${session.id}`} prefetch={false}> <Link href={`/work-sessions/${session.id}`} prefetch={false}>
<Button variant="outline" size="sm" className="gap-2"> <Button variant="outline" size="sm" className="gap-2">
<ExternalLink className="h-3 w-3" /> <ExternalLink className="h-3 w-3" />
View Details View Details
</Button> </Button>
</Link> </Link>
</HelpTip>
</div> </div>
</CardContent> </CardContent>
</Card> </Card>
@@ -143,15 +143,18 @@ export function TaskFilters({
<CardContent className="pt-6"> <CardContent className="pt-6">
<div className="flex flex-col sm:flex-row gap-4"> <div className="flex flex-col sm:flex-row gap-4">
<div className="flex-1"> <div className="flex-1">
<HelpTip label="Debounced server-side search across title, description, and task ID prefix.">
<Input <Input
placeholder="Search tasks..." placeholder="Search tasks..."
value={searchQuery} value={searchQuery}
onChange={(e) => onSearchChange(e.target.value)} onChange={(e) => onSearchChange(e.target.value)}
/> />
</HelpTip>
</div> </div>
<div className="flex flex-wrap gap-2"> <div className="flex flex-wrap gap-2">
{/* Status Multi-Select */} {/* Status Multi-Select */}
<Popover> <Popover>
<HelpTip label="Multi-select — pick any number of statuses; the list shows tasks matching any of them.">
<PopoverTrigger asChild> <PopoverTrigger asChild>
<Button variant="outline" className="min-w-32 justify-between"> <Button variant="outline" className="min-w-32 justify-between">
<span className="truncate"> <span className="truncate">
@@ -164,6 +167,7 @@ export function TaskFilters({
<ChevronDown className="ml-2 h-4 w-4 shrink-0 opacity-50" /> <ChevronDown className="ml-2 h-4 w-4 shrink-0 opacity-50" />
</Button> </Button>
</PopoverTrigger> </PopoverTrigger>
</HelpTip>
<PopoverContent className="w-56 p-2" align="start"> <PopoverContent className="w-56 p-2" align="start">
<div className="flex items-center justify-between mb-2 pb-2 border-b"> <div className="flex items-center justify-between mb-2 pb-2 border-b">
<span className="text-sm font-medium">Status</span> <span className="text-sm font-medium">Status</span>
@@ -197,6 +201,7 @@ export function TaskFilters({
{/* Team Multi-Select */} {/* Team Multi-Select */}
<Popover> <Popover>
<HelpTip label="Multi-select — pick any number of teams; the list shows tasks matching any of them.">
<PopoverTrigger asChild> <PopoverTrigger asChild>
<Button variant="outline" className="min-w-32 justify-between"> <Button variant="outline" className="min-w-32 justify-between">
<span className="truncate"> <span className="truncate">
@@ -209,6 +214,7 @@ export function TaskFilters({
<ChevronDown className="ml-2 h-4 w-4 shrink-0 opacity-50" /> <ChevronDown className="ml-2 h-4 w-4 shrink-0 opacity-50" />
</Button> </Button>
</PopoverTrigger> </PopoverTrigger>
</HelpTip>
<PopoverContent className="w-48 p-2" align="start"> <PopoverContent className="w-48 p-2" align="start">
<div className="flex items-center justify-between mb-2 pb-2 border-b"> <div className="flex items-center justify-between mb-2 pb-2 border-b">
<span className="text-sm font-medium">Team</span> <span className="text-sm font-medium">Team</span>
@@ -243,6 +249,7 @@ export function TaskFilters({
{/* Task Type Multi-Select (optional) */} {/* Task Type Multi-Select (optional) */}
{onTaskTypeChange && ( {onTaskTypeChange && (
<Popover> <Popover>
<HelpTip label="Multi-select — pick any number of task types; the list shows tasks matching any of them.">
<PopoverTrigger asChild> <PopoverTrigger asChild>
<Button <Button
variant="outline" variant="outline"
@@ -258,6 +265,7 @@ export function TaskFilters({
<ChevronDown className="ml-2 h-4 w-4 shrink-0 opacity-50" /> <ChevronDown className="ml-2 h-4 w-4 shrink-0 opacity-50" />
</Button> </Button>
</PopoverTrigger> </PopoverTrigger>
</HelpTip>
<PopoverContent className="w-48 p-2" align="start"> <PopoverContent className="w-48 p-2" align="start">
<div className="flex items-center justify-between mb-2 pb-2 border-b"> <div className="flex items-center justify-between mb-2 pb-2 border-b">
<span className="text-sm font-medium">Task Type</span> <span className="text-sm font-medium">Task Type</span>
@@ -295,6 +303,7 @@ export function TaskFilters({
{/* Project Multi-Select (optional) */} {/* Project Multi-Select (optional) */}
{onProjectChange && projectOptions.length > 0 && ( {onProjectChange && projectOptions.length > 0 && (
<Popover> <Popover>
<HelpTip label="Multi-select — pick any number of projects; the list shows tasks matching any of them.">
<PopoverTrigger asChild> <PopoverTrigger asChild>
<Button <Button
variant="outline" variant="outline"
@@ -310,6 +319,7 @@ export function TaskFilters({
<ChevronDown className="ml-2 h-4 w-4 shrink-0 opacity-50" /> <ChevronDown className="ml-2 h-4 w-4 shrink-0 opacity-50" />
</Button> </Button>
</PopoverTrigger> </PopoverTrigger>
</HelpTip>
<PopoverContent className="w-56 p-2" align="start"> <PopoverContent className="w-56 p-2" align="start">
<div className="flex items-center justify-between mb-2 pb-2 border-b"> <div className="flex items-center justify-between mb-2 pb-2 border-b">
<span className="text-sm font-medium">Project</span> <span className="text-sm font-medium">Project</span>
@@ -345,6 +355,7 @@ export function TaskFilters({
{/* Product Multi-Select (optional) */} {/* Product Multi-Select (optional) */}
{onProductChange && productOptions.length > 0 && ( {onProductChange && productOptions.length > 0 && (
<Popover> <Popover>
<HelpTip label="Multi-select — pick any number of products; the list shows tasks matching any of them.">
<PopoverTrigger asChild> <PopoverTrigger asChild>
<Button <Button
variant="outline" variant="outline"
@@ -360,6 +371,7 @@ export function TaskFilters({
<ChevronDown className="ml-2 h-4 w-4 shrink-0 opacity-50" /> <ChevronDown className="ml-2 h-4 w-4 shrink-0 opacity-50" />
</Button> </Button>
</PopoverTrigger> </PopoverTrigger>
</HelpTip>
<PopoverContent className="w-56 p-2" align="start"> <PopoverContent className="w-56 p-2" align="start">
<div className="flex items-center justify-between mb-2 pb-2 border-b"> <div className="flex items-center justify-between mb-2 pb-2 border-b">
<span className="text-sm font-medium">Product</span> <span className="text-sm font-medium">Product</span>
@@ -466,6 +478,7 @@ export function TaskFilters({
taskTypeFilter.length > 0 || taskTypeFilter.length > 0 ||
projectFilter.length > 0 || projectFilter.length > 0 ||
productFilter.length > 0) && ( productFilter.length > 0) && (
<HelpTip label="Removes every active filter above, restoring the unfiltered task list.">
<Button <Button
variant="ghost" variant="ghost"
size="sm" size="sm"
@@ -480,6 +493,7 @@ export function TaskFilters({
> >
Clear all Clear all
</Button> </Button>
</HelpTip>
)} )}
</div> </div>
)} )}
@@ -151,10 +151,12 @@ export function TaskSelector({
<SelectContent className="max-h-[300px]"> <SelectContent className="max-h-[300px]">
{allowClear && value && ( {allowClear && value && (
<SelectItem value="__clear__" className="text-muted-foreground"> <SelectItem value="__clear__" className="text-muted-foreground">
<HelpTip label="Clears the parent link — this becomes a standalone root task.">
<span className="flex items-center gap-2"> <span className="flex items-center gap-2">
<X className="h-4 w-4" /> <X className="h-4 w-4" />
No parent (standalone task) No parent (standalone task)
</span> </span>
</HelpTip>
</SelectItem> </SelectItem>
)} )}
+79 -1
View File
@@ -244,16 +244,20 @@ function SortableHeader({
return ( return (
<TableHead className={className}> <TableHead className={className}>
<HelpTip label="Sorts ascending, then descending, then clears — click again to cycle.">
<Button <Button
onClick={() => onSort(field)} onClick={() => onSort(field)}
variant="ghost" variant="ghost"
className="h-auto -ml-2 px-2 py-1 gap-1 font-normal" className="h-auto -ml-2 px-2 py-1 gap-1 font-normal"
> >
{label} {label}
{!isActive && <ArrowUpDown className="h-4 w-4 text-muted-foreground" />} {!isActive && (
<ArrowUpDown className="h-4 w-4 text-muted-foreground" />
)}
{direction === "asc" && <ArrowUp className="h-4 w-4" />} {direction === "asc" && <ArrowUp className="h-4 w-4" />}
{direction === "desc" && <ArrowDown className="h-4 w-4" />} {direction === "desc" && <ArrowDown className="h-4 w-4" />}
</Button> </Button>
</HelpTip>
</TableHead> </TableHead>
); );
} }
@@ -473,6 +477,7 @@ export function TaskTable({
{hasAnyChildren && !isLoading && ( {hasAnyChildren && !isLoading && (
<div className="flex items-center gap-2 px-4 py-2 border-b bg-muted/30"> <div className="flex items-center gap-2 px-4 py-2 border-b bg-muted/30">
<span className="text-sm text-muted-foreground">Tree view:</span> <span className="text-sm text-muted-foreground">Tree view:</span>
<HelpTip label="Opens every parent row so all subtasks show inline, ignoring current sort.">
<Button <Button
variant="ghost" variant="ghost"
size="sm" size="sm"
@@ -481,6 +486,8 @@ export function TaskTable({
> >
Expand all Expand all
</Button> </Button>
</HelpTip>
<HelpTip label="Hides every subtask row, showing only root-level tasks.">
<Button <Button
variant="ghost" variant="ghost"
size="sm" size="sm"
@@ -489,6 +496,7 @@ export function TaskTable({
> >
Collapse all Collapse all
</Button> </Button>
</HelpTip>
</div> </div>
)} )}
@@ -510,7 +518,9 @@ export function TaskTable({
onSort={handleSort} onSort={handleSort}
className="whitespace-nowrap" className="whitespace-nowrap"
/> />
<HelpTip label="Branch, PR, or docs/PR progress — whichever this task's git workflow has reached.">
<TableHead className="whitespace-nowrap">Git</TableHead> <TableHead className="whitespace-nowrap">Git</TableHead>
</HelpTip>
<SortableHeader <SortableHeader
label="Team" label="Team"
field="team" field="team"
@@ -518,9 +528,11 @@ export function TaskTable({
onSort={handleSort} onSort={handleSort}
className="whitespace-nowrap" className="whitespace-nowrap"
/> />
<HelpTip label="A direct Project shows its name; a fan-out task shows its Product instead.">
<TableHead className="whitespace-nowrap"> <TableHead className="whitespace-nowrap">
Project / Product Project / Product
</TableHead> </TableHead>
</HelpTip>
<SortableHeader <SortableHeader
label="Priority" label="Priority"
field="priority" field="priority"
@@ -589,6 +601,13 @@ export function TaskTable({
style={{ paddingLeft: `${node.depth * 1.5}rem` }} style={{ paddingLeft: `${node.depth * 1.5}rem` }}
> >
{hasChildren ? ( {hasChildren ? (
<HelpTip
label={
isExpanded
? "Hides this task's subtasks"
: "Shows this task's subtasks inline"
}
>
<Button <Button
onClick={() => toggleExpand(task.id)} onClick={() => toggleExpand(task.id)}
variant="ghost" variant="ghost"
@@ -602,6 +621,7 @@ export function TaskTable({
<ChevronRightIcon className="h-4 w-4" /> <ChevronRightIcon className="h-4 w-4" />
)} )}
</Button> </Button>
</HelpTip>
) : ( ) : (
<span className="w-5 shrink-0" /> <span className="w-5 shrink-0" />
)} )}
@@ -625,6 +645,7 @@ export function TaskTable({
</HelpTip> </HelpTip>
)} )}
{childCount > 0 && ( {childCount > 0 && (
<HelpTip label="Direct subtasks under this task, regardless of their current status.">
<Badge <Badge
variant="secondary" variant="secondary"
className="text-xs shrink-0" className="text-xs shrink-0"
@@ -632,6 +653,7 @@ export function TaskTable({
{childCount} subtask {childCount} subtask
{childCount !== 1 ? "s" : ""} {childCount !== 1 ? "s" : ""}
</Badge> </Badge>
</HelpTip>
)} )}
</div> </div>
</Link> </Link>
@@ -677,14 +699,28 @@ export function TaskTable({
</Badge> </Badge>
</TableCell> </TableCell>
<TableCell className="whitespace-nowrap"> <TableCell className="whitespace-nowrap">
<HelpTip
label={
task.assigned_to
? "The agent currently pinned to this task."
: "No agent pinned — the orchestrator routes it by role and availability."
}
>
<Badge variant="outline"> <Badge variant="outline">
{getAgentDisplayName(task.assigned_to)} {getAgentDisplayName(task.assigned_to)}
</Badge> </Badge>
</HelpTip>
</TableCell> </TableCell>
<TableCell className="text-muted-foreground text-sm whitespace-nowrap"> <TableCell className="text-muted-foreground text-sm whitespace-nowrap">
<HelpTip
label={new Date(task.created_at).toLocaleString()}
>
<span>
{formatDistanceToNow(new Date(task.created_at), { {formatDistanceToNow(new Date(task.created_at), {
addSuffix: true, addSuffix: true,
})} })}
</span>
</HelpTip>
</TableCell> </TableCell>
<TableCell> <TableCell>
<TaskActions task={task} /> <TaskActions task={task} />
@@ -720,6 +756,13 @@ export function TaskTable({
<div className="min-w-0 flex-1"> <div className="min-w-0 flex-1">
<div className="flex items-center gap-1.5"> <div className="flex items-center gap-1.5">
{hasChildren && ( {hasChildren && (
<HelpTip
label={
isExpanded
? "Hides this task's subtasks"
: "Shows this task's subtasks inline"
}
>
<Button <Button
onClick={() => toggleExpand(task.id)} onClick={() => toggleExpand(task.id)}
variant="ghost" variant="ghost"
@@ -733,6 +776,7 @@ export function TaskTable({
<ChevronRightIcon className="h-4 w-4" /> <ChevronRightIcon className="h-4 w-4" />
)} )}
</Button> </Button>
</HelpTip>
)} )}
<Link <Link
prefetch={false} prefetch={false}
@@ -757,10 +801,12 @@ export function TaskTable({
</HelpTip> </HelpTip>
)} )}
{childCount > 0 && ( {childCount > 0 && (
<HelpTip label="Direct subtasks under this task, regardless of their current status.">
<Badge variant="secondary" className="text-xs"> <Badge variant="secondary" className="text-xs">
{childCount} subtask {childCount} subtask
{childCount !== 1 ? "s" : ""} {childCount !== 1 ? "s" : ""}
</Badge> </Badge>
</HelpTip>
)} )}
</div> </div>
) : null} ) : null}
@@ -805,14 +851,28 @@ export function TaskTable({
</Badge> </Badge>
</ResponsiveTableCardRow> </ResponsiveTableCardRow>
<ResponsiveTableCardRow label="Assigned"> <ResponsiveTableCardRow label="Assigned">
<HelpTip
label={
task.assigned_to
? "The agent currently pinned to this task."
: "No agent pinned — the orchestrator routes it by role and availability."
}
>
<Badge variant="outline"> <Badge variant="outline">
{getAgentDisplayName(task.assigned_to)} {getAgentDisplayName(task.assigned_to)}
</Badge> </Badge>
</HelpTip>
</ResponsiveTableCardRow> </ResponsiveTableCardRow>
<ResponsiveTableCardRow label="Created"> <ResponsiveTableCardRow label="Created">
<HelpTip
label={new Date(task.created_at).toLocaleString()}
>
<span>
{formatDistanceToNow(new Date(task.created_at), { {formatDistanceToNow(new Date(task.created_at), {
addSuffix: true, addSuffix: true,
})} })}
</span>
</HelpTip>
</ResponsiveTableCardRow> </ResponsiveTableCardRow>
</div> </div>
</ResponsiveTableCard> </ResponsiveTableCard>
@@ -827,7 +887,9 @@ export function TaskTable({
{!isLoading && tasks && tasks.length > 0 && ( {!isLoading && tasks && tasks.length > 0 && (
<div className="flex flex-wrap items-center justify-end gap-4 px-4 py-3 border-t"> <div className="flex flex-wrap items-center justify-end gap-4 px-4 py-3 border-t">
<div className="flex items-center gap-2 text-sm text-muted-foreground"> <div className="flex items-center gap-2 text-sm text-muted-foreground">
<HelpTip label="How many rows show per page; resets to page 1 when changed.">
<span>Rows:</span> <span>Rows:</span>
</HelpTip>
<Select <Select
value={String(pageSize)} value={String(pageSize)}
onValueChange={handlePageSizeChange} onValueChange={handlePageSizeChange}
@@ -844,10 +906,17 @@ export function TaskTable({
</SelectContent> </SelectContent>
</Select> </Select>
</div> </div>
<HelpTip label="Range within the current filtered/sorted, flattened tree view — not the total unfiltered task count.">
<span className="text-sm text-muted-foreground"> <span className="text-sm text-muted-foreground">
{startIndex + 1}-{endIndex} of {totalItems} {startIndex + 1}-{endIndex} of {totalItems}
</span> </span>
</HelpTip>
<div className="flex items-center gap-1"> <div className="flex items-center gap-1">
<HelpTip label="Previous page.">
<span
className="inline-block"
tabIndex={currentPage === 1 ? 0 : undefined}
>
<Button <Button
variant="outline" variant="outline"
size="icon" size="icon"
@@ -858,6 +927,13 @@ export function TaskTable({
> >
<ChevronLeft className="h-4 w-4" /> <ChevronLeft className="h-4 w-4" />
</Button> </Button>
</span>
</HelpTip>
<HelpTip label="Next page.">
<span
className="inline-block"
tabIndex={currentPage === totalPages ? 0 : undefined}
>
<Button <Button
variant="outline" variant="outline"
size="icon" size="icon"
@@ -868,6 +944,8 @@ export function TaskTable({
> >
<ChevronRight className="h-4 w-4" /> <ChevronRight className="h-4 w-4" />
</Button> </Button>
</span>
</HelpTip>
</div> </div>
</div> </div>
)} )}
+15 -10
View File
@@ -2,11 +2,7 @@
import { TaskType } from "@/types"; import { TaskType } from "@/types";
import { Badge } from "@/components/ui/badge"; import { Badge } from "@/components/ui/badge";
import { import { HelpTip } from "@/components/ui/help-tip";
Tooltip,
TooltipContent,
TooltipTrigger,
} from "@/components/ui/tooltip";
import { import {
Code, Code,
FileText, FileText,
@@ -22,6 +18,18 @@ interface TaskTypeBadgeProps {
className?: string; className?: string;
} }
// What each task type classifies as its artifact kind. Every type still
// follows the identical full git workflow (branch, commits, PR) — this is
// classification only, not a different pipeline.
const TYPE_DESCRIPTIONS: Record<TaskType, string> = {
[TaskType.CODE]: "Source code changes.",
[TaskType.DOCUMENTATION]: "Documentation updates.",
[TaskType.RESEARCH]: "Research findings, committed as notes.",
[TaskType.PLANNING]: "Plans or architecture, committed as docs.",
[TaskType.DESIGN]: "Designs or specs, committed as assets.",
[TaskType.ADMINISTRATIVE]: "Process docs, committed as notes.",
};
const TYPE_CONFIG: Record< const TYPE_CONFIG: Record<
TaskType, TaskType,
{ label: string; icon: React.ReactNode; color: string } { label: string; icon: React.ReactNode; color: string }
@@ -71,16 +79,13 @@ export function TaskTypeBadge({
if (!config) return null; if (!config) return null;
return ( return (
<Tooltip> <HelpTip label={TYPE_DESCRIPTIONS[type]}>
<TooltipTrigger asChild>
<Badge variant="outline" className={`${config.color} ${className}`}> <Badge variant="outline" className={`${config.color} ${className}`}>
<span className="flex items-center gap-1"> <span className="flex items-center gap-1">
{config.icon} {config.icon}
{showLabel && <span>{config.label}</span>} {showLabel && <span>{config.label}</span>}
</span> </span>
</Badge> </Badge>
</TooltipTrigger> </HelpTip>
<TooltipContent>Task type: {config.label}</TooltipContent>
</Tooltip>
); );
} }
@@ -13,6 +13,7 @@ import {
import { Badge } from "@/components/ui/badge"; import { Badge } from "@/components/ui/badge";
import { Search, Filter, X } from "lucide-react"; import { Search, Filter, X } from "lucide-react";
import { WorkSessionStatus } from "@/types"; import { WorkSessionStatus } from "@/types";
import { HelpTip } from "@/components/ui/help-tip";
interface WorkSessionFiltersProps { interface WorkSessionFiltersProps {
searchQuery: string; searchQuery: string;
@@ -21,10 +22,22 @@ interface WorkSessionFiltersProps {
onStatusChange: (value: WorkSessionStatus[]) => void; onStatusChange: (value: WorkSessionStatus[]) => void;
} }
const statuses: { value: WorkSessionStatus; label: string }[] = [ const statuses: { value: WorkSessionStatus; label: string; hint: string }[] = [
{ value: WorkSessionStatus.ACTIVE, label: "Active" }, {
{ value: WorkSessionStatus.COMPLETED, label: "Completed" }, value: WorkSessionStatus.ACTIVE,
{ value: WorkSessionStatus.ABANDONED, label: "Abandoned" }, 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({ export function WorkSessionFilters({
@@ -51,6 +64,7 @@ export function WorkSessionFilters({
return ( return (
<div className="flex flex-col gap-4 sm:flex-row sm:items-center"> <div className="flex flex-col gap-4 sm:flex-row sm:items-center">
{/* Search */} {/* Search */}
<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"> <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" /> <Search className="absolute left-3 top-1/2 h-4 w-4 -translate-y-1/2 text-muted-foreground" />
<Input <Input
@@ -60,9 +74,14 @@ export function WorkSessionFilters({
className="pl-9" className="pl-9"
/> />
</div> </div>
</HelpTip>
{/* Status Filter */} {/* Status Filter */}
<DropdownMenu> <DropdownMenu>
{/* 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> <DropdownMenuTrigger asChild>
<Button variant="outline" className="gap-2"> <Button variant="outline" className="gap-2">
<Filter className="h-4 w-4" /> <Filter className="h-4 w-4" />
@@ -74,6 +93,7 @@ export function WorkSessionFilters({
)} )}
</Button> </Button>
</DropdownMenuTrigger> </DropdownMenuTrigger>
</HelpTip>
<DropdownMenuContent align="start" className="w-48"> <DropdownMenuContent align="start" className="w-48">
<DropdownMenuLabel>Filter by Status</DropdownMenuLabel> <DropdownMenuLabel>Filter by Status</DropdownMenuLabel>
<DropdownMenuSeparator /> <DropdownMenuSeparator />
@@ -83,7 +103,9 @@ export function WorkSessionFilters({
checked={statusFilter.includes(status.value)} checked={statusFilter.includes(status.value)}
onCheckedChange={() => handleStatusToggle(status.value)} onCheckedChange={() => handleStatusToggle(status.value)}
> >
{status.label} <HelpTip label={status.hint}>
<span className="w-fit">{status.label}</span>
</HelpTip>
</DropdownMenuCheckboxItem> </DropdownMenuCheckboxItem>
))} ))}
</DropdownMenuContent> </DropdownMenuContent>
@@ -91,10 +113,12 @@ export function WorkSessionFilters({
{/* Clear Filters */} {/* Clear Filters */}
{hasActiveFilters && ( {hasActiveFilters && (
<HelpTip label="Resets the search text and status filter — shows every work session again.">
<Button variant="ghost" onClick={clearFilters} className="gap-2"> <Button variant="ghost" onClick={clearFilters} className="gap-2">
<X className="h-4 w-4" /> <X className="h-4 w-4" />
Clear Clear
</Button> </Button>
</HelpTip>
)} )}
</div> </div>
); );
@@ -24,6 +24,7 @@ import {
ResponsiveTableCardRow, ResponsiveTableCardRow,
} from "@/components/ui/responsive-table"; } from "@/components/ui/responsive-table";
import { Skeleton } from "@/components/ui/skeleton"; import { Skeleton } from "@/components/ui/skeleton";
import { HelpTip } from "@/components/ui/help-tip";
import { import {
GitBranch, GitBranch,
GitPullRequest, GitPullRequest,
@@ -40,6 +41,15 @@ interface WorkSessionTableProps {
isLoading: boolean; 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) { function getStatusBadge(status: WorkSessionStatus) {
switch (status) { switch (status) {
case "active": 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({ export function WorkSessionTable({
sessions, sessions,
isLoading, isLoading,
@@ -115,6 +148,7 @@ export function WorkSessionTable({
<TableCell> <TableCell>
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<GitBranch className="h-4 w-4 text-muted-foreground" /> <GitBranch className="h-4 w-4 text-muted-foreground" />
<HelpTip label="Open this session's detail page.">
<Link <Link
prefetch={false} prefetch={false}
href={`/work-sessions/${session.id}`} href={`/work-sessions/${session.id}`}
@@ -122,35 +156,36 @@ export function WorkSessionTable({
> >
{session.branch_name} {session.branch_name}
</Link> </Link>
</HelpTip>
</div> </div>
</TableCell> </TableCell>
<TableCell> <TableCell>
<HelpTip label={`Open task ${session.task_id}`}>
<Link <Link
prefetch={false} prefetch={false}
href={`/tasks/${session.task_id}`} href={`/tasks/${session.task_id}`}
className="text-sm text-muted-foreground hover:text-foreground hover:underline" className="text-sm text-muted-foreground hover:text-foreground hover:underline"
title={session.task_id}
> >
{session.task_id.slice(0, 8)}... {session.task_id.slice(0, 8)}...
</Link> </Link>
</HelpTip>
</TableCell> </TableCell>
<TableCell>{getStatusBadge(session.status)}</TableCell>
<TableCell> <TableCell>
{session.has_pr ? ( <StatusBadge status={session.status} />
<Badge className="bg-purple-500/10 text-purple-500"> </TableCell>
<GitPullRequest className="h-3 w-3 mr-1" /> <TableCell>
PR Open <PrBadge hasPr={session.has_pr} />
</Badge>
) : (
<span className="text-sm text-muted-foreground">
No PR
</span>
)}
</TableCell> </TableCell>
<TableCell className="text-sm text-muted-foreground"> <TableCell className="text-sm text-muted-foreground">
<HelpTip
label={new Date(session.started_at).toLocaleString()}
>
<span className="w-fit">
{formatDistanceToNow(new Date(session.started_at), { {formatDistanceToNow(new Date(session.started_at), {
addSuffix: true, addSuffix: true,
})} })}
</span>
</HelpTip>
</TableCell> </TableCell>
<TableCell> <TableCell>
<Tooltip> <Tooltip>
@@ -180,14 +215,17 @@ export function WorkSessionTable({
<div className="flex items-start justify-between gap-2"> <div className="flex items-start justify-between gap-2">
<div className="flex min-w-0 items-center gap-2"> <div className="flex min-w-0 items-center gap-2">
<GitBranch className="h-4 w-4 shrink-0 text-muted-foreground" /> <GitBranch className="h-4 w-4 shrink-0 text-muted-foreground" />
<HelpTip
label={`Open this session's detail page — ${session.branch_name}`}
>
<Link <Link
prefetch={false} prefetch={false}
href={`/work-sessions/${session.id}`} href={`/work-sessions/${session.id}`}
className="truncate font-mono text-sm font-medium hover:underline" className="truncate font-mono text-sm font-medium hover:underline"
title={session.branch_name}
> >
{session.branch_name} {session.branch_name}
</Link> </Link>
</HelpTip>
</div> </div>
<Tooltip> <Tooltip>
<TooltipTrigger asChild> <TooltipTrigger asChild>
@@ -205,32 +243,32 @@ export function WorkSessionTable({
</div> </div>
<div className="mt-3 divide-y"> <div className="mt-3 divide-y">
<ResponsiveTableCardRow label="Task"> <ResponsiveTableCardRow label="Task">
<HelpTip label={`Open task ${session.task_id}`}>
<Link <Link
prefetch={false} prefetch={false}
href={`/tasks/${session.task_id}`} href={`/tasks/${session.task_id}`}
className="text-muted-foreground hover:text-foreground hover:underline" className="text-muted-foreground hover:text-foreground hover:underline"
title={session.task_id}
> >
{session.task_id.slice(0, 8)}... {session.task_id.slice(0, 8)}...
</Link> </Link>
</HelpTip>
</ResponsiveTableCardRow> </ResponsiveTableCardRow>
<ResponsiveTableCardRow label="Status"> <ResponsiveTableCardRow label="Status">
{getStatusBadge(session.status)} <StatusBadge status={session.status} />
</ResponsiveTableCardRow> </ResponsiveTableCardRow>
<ResponsiveTableCardRow label="PR"> <ResponsiveTableCardRow label="PR">
{session.has_pr ? ( <PrBadge hasPr={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>
)}
</ResponsiveTableCardRow> </ResponsiveTableCardRow>
<ResponsiveTableCardRow label="Started"> <ResponsiveTableCardRow label="Started">
<HelpTip
label={new Date(session.started_at).toLocaleString()}
>
<span className="w-fit">
{formatDistanceToNow(new Date(session.started_at), { {formatDistanceToNow(new Date(session.started_at), {
addSuffix: true, addSuffix: true,
})} })}
</span>
</HelpTip>
</ResponsiveTableCardRow> </ResponsiveTableCardRow>
</div> </div>
</ResponsiveTableCard> </ResponsiveTableCard>