feat: Add compact mode to TaskGraph, use in pipeline tab

TaskGraph now accepts a `compact` prop that hides the second line
(category badge, priority) and uses tighter vertical padding. Blocked-by
count moves inline on the first line as "N deps". Pipeline phase cards
pass compact; the plan tab's phase detail keeps the full two-line layout.
This commit is contained in:
Lukas May
2026-03-04 13:16:37 +01:00
parent 087f6945ae
commit 748f0c294a
2 changed files with 43 additions and 24 deletions

View File

@@ -25,6 +25,8 @@ interface TaskGraphProps {
agentNameMap: Map<string, string>; agentNameMap: Map<string, string>;
onClickTask: (taskId: string) => void; onClickTask: (taskId: string) => void;
onDeleteTask?: (taskId: string) => void; onDeleteTask?: (taskId: string) => void;
/** Hide category badge and priority — use in compact contexts like the pipeline */
compact?: boolean;
} }
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
@@ -51,6 +53,7 @@ export function TaskGraph({
agentNameMap, agentNameMap,
onClickTask, onClickTask,
onDeleteTask, onDeleteTask,
compact = false,
}: TaskGraphProps) { }: TaskGraphProps) {
// Flatten task dep edges to DependencyEdge format for reuse of groupPhasesByDependencyLevel // Flatten task dep edges to DependencyEdge format for reuse of groupPhasesByDependencyLevel
const columns = useMemo(() => { const columns = useMemo(() => {
@@ -112,6 +115,7 @@ export function TaskGraph({
? () => onDeleteTask(task.id) ? () => onDeleteTask(task.id)
: undefined : undefined
} }
compact={compact}
/> />
))} ))}
</div> </div>
@@ -127,6 +131,7 @@ export function TaskGraph({
? () => onDeleteTask(col.phases[0].id) ? () => onDeleteTask(col.phases[0].id)
: undefined : undefined
} }
compact={compact}
/> />
)} )}
</div> </div>
@@ -146,12 +151,14 @@ function TaskNode({
agentName, agentName,
onClick, onClick,
onDelete, onDelete,
compact = false,
}: { }: {
task: SerializedTask; task: SerializedTask;
blockedByCount: number; blockedByCount: number;
agentName?: string; agentName?: string;
onClick: () => void; onClick: () => void;
onDelete?: () => void; onDelete?: () => void;
compact?: boolean;
}) { }) {
const variant = mapEntityStatus(task.status); const variant = mapEntityStatus(task.status);
const catConfig = getCategoryConfig(task.category); const catConfig = getCategoryConfig(task.category);
@@ -163,10 +170,14 @@ function TaskNode({
? "text-status-warning-fg" ? "text-status-warning-fg"
: null; : null;
const hasSecondLine =
!compact && (true /* category badge always shows */ || priorityColor || agentName || blockedByCount > 0);
return ( return (
<div <div
className={cn( className={cn(
"group flex w-full cursor-pointer gap-2 rounded-md px-2 py-1.5 text-left transition-all hover:bg-accent/50", "group flex w-full cursor-pointer gap-2 rounded-md px-2 text-left transition-all hover:bg-accent/50",
compact ? "py-1" : "py-1.5",
)} )}
onClick={onClick} onClick={onClick}
> >
@@ -179,13 +190,18 @@ function TaskNode({
)} )}
/> />
{/* Two-line content */} {/* Content */}
<div className="min-w-0 flex-1"> <div className="min-w-0 flex-1">
{/* Line 1: name + status + delete */} {/* Line 1: name + status + blocked count (compact) + delete */}
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<span className="min-w-0 flex-1 truncate text-[13px]"> <span className="min-w-0 flex-1 truncate text-[13px]">
{task.name} {task.name}
</span> </span>
{compact && blockedByCount > 0 && (
<span className="shrink-0 text-[10px] text-muted-foreground">
{blockedByCount} dep{blockedByCount === 1 ? "" : "s"}
</span>
)}
<StatusBadge <StatusBadge
status={task.status} status={task.status}
className="shrink-0 text-[9px]" className="shrink-0 text-[9px]"
@@ -206,27 +222,29 @@ function TaskNode({
)} )}
</div> </div>
{/* Line 2: category + priority + agent + blocked count */} {/* Line 2: category + priority + agent + blocked count (full mode only) */}
<div className="mt-0.5 flex items-center gap-1.5"> {hasSecondLine && (
<Badge variant={catConfig.variant} size="xs"> <div className="mt-0.5 flex items-center gap-1.5">
{catConfig.label} <Badge variant={catConfig.variant} size="xs">
</Badge> {catConfig.label}
{priorityColor && ( </Badge>
<span className={cn("text-[10px] font-medium capitalize", priorityColor)}> {priorityColor && (
{task.priority} <span className={cn("text-[10px] font-medium capitalize", priorityColor)}>
</span> {task.priority}
)} </span>
{agentName && ( )}
<span className="truncate text-[10px] text-muted-foreground"> {agentName && (
{agentName} <span className="truncate text-[10px] text-muted-foreground">
</span> {agentName}
)} </span>
{blockedByCount > 0 && ( )}
<span className="text-[10px] text-muted-foreground"> {blockedByCount > 0 && (
blocked by {blockedByCount} <span className="text-[10px] text-muted-foreground">
</span> blocked by {blockedByCount}
)} </span>
</div> )}
</div>
)}
</div> </div>
</div> </div>
); );

View File

@@ -141,6 +141,7 @@ export function PipelinePhaseGroup({ phase, tasks, taskDepsRaw, isBlocked, detai
blockedByCountMap={blockedByCountMap} blockedByCountMap={blockedByCountMap}
agentNameMap={emptyAgentMap} agentNameMap={emptyAgentMap}
onClickTask={setSelectedTaskId} onClickTask={setSelectedTaskId}
compact
/> />
)} )}
</div> </div>