mirror of
https://github.com/rennf93/roboco.git
synced 2026-08-03 07:23:24 +02:00
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:
@@ -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>
|
||||||
|
|||||||
@@ -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", () => {
|
||||||
|
|||||||
@@ -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>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -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}
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|
||||||
|
|||||||
@@ -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">
|
||||||
|
|||||||
@@ -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 & Start
|
Approve & 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 & Head of Marketing)</Label>
|
<Label>Board review (Product Owner & 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
|
||||||
|
source→target 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>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
|||||||
@@ -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>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
Reference in New Issue
Block a user