Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions web-ui/src/components/tasks/TaskBoardView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -376,6 +376,7 @@ export function TaskBoardView({ workspacePath }: TaskBoardViewProps) {
onClose={handleCloseDetail}
onExecute={handleExecute}
onStatusChange={handleStatusChange}
onOpenTask={handleTaskClick}
/>

{/* Bulk action confirmation */}
Expand Down
18 changes: 14 additions & 4 deletions web-ui/src/components/tasks/TaskCard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { Card, CardContent } from '@/components/ui/card';
import { Badge } from '@/components/ui/badge';
import { Button } from '@/components/ui/button';
import { Checkbox } from '@/components/ui/checkbox';
import { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider } from '@/components/ui/tooltip';
import type { Task, TaskStatus, ProofRequirement } from '@/types';

/** Map backend TaskStatus to badge variant name. */
Expand Down Expand Up @@ -97,10 +98,19 @@ export function TaskCard({
</Badge>
</div>
{task.depends_on.length > 0 && (
<span className="flex items-center gap-1 text-xs text-muted-foreground" title={`Depends on ${task.depends_on.length} task(s)`}>
<LinkCircleIcon className="h-3.5 w-3.5" />
{task.depends_on.length}
</span>
<TooltipProvider>
<Tooltip>
<TooltipTrigger asChild>
<span className="flex cursor-default items-center gap-1 text-xs text-muted-foreground">
<LinkCircleIcon className="h-3.5 w-3.5" />
{task.depends_on.length}
</span>
</TooltipTrigger>
<TooltipContent>
Depends on {task.depends_on.length} task{task.depends_on.length !== 1 ? 's' : ''}. This task will become READY when all dependencies complete.
</TooltipContent>
</Tooltip>
</TooltipProvider>
)}
</div>

Expand Down
56 changes: 48 additions & 8 deletions web-ui/src/components/tasks/TaskDetailModal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -24,9 +24,10 @@ import {
} from '@/components/ui/dialog';
import { Badge } from '@/components/ui/badge';
import { Button } from '@/components/ui/button';
import useSWR from 'swr';
import { tasksApi } from '@/lib/api';
import { useRequirementsLookup } from '@/hooks/useRequirementsLookup';
import type { Task, TaskStatus, ApiError } from '@/types';
import type { Task, TaskStatus, ApiError, TaskListResponse } from '@/types';

const STATUS_BADGE_VARIANT: Record<TaskStatus, string> = {
BACKLOG: 'backlog',
Expand Down Expand Up @@ -55,6 +56,7 @@ interface TaskDetailModalProps {
onClose: () => void;
onExecute: (taskId: string) => void;
onStatusChange: () => void;
onOpenTask?: (taskId: string) => void;
}

export function TaskDetailModal({
Expand All @@ -64,6 +66,7 @@ export function TaskDetailModal({
onClose,
onExecute,
onStatusChange,
onOpenTask,
}: TaskDetailModalProps) {
const router = useRouter();
const [task, setTask] = useState<Task | null>(null);
Expand All @@ -72,6 +75,13 @@ export function TaskDetailModal({
const [isUpdating, setIsUpdating] = useState(false);
const { requirementsMap, isLoading: reqsLoading } = useRequirementsLookup(workspacePath);

// Fetch all tasks (reuses TaskBoardView's SWR cache — same key, no extra network call)
const { data: allTasksData } = useSWR<TaskListResponse>(
`/api/v2/tasks?path=${workspacePath}`,
() => tasksApi.getAll(workspacePath)
);
const tasksById = new Map(allTasksData?.tasks.map((t) => [t.id, t]) ?? []);

Comment on lines +78 to +84

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Render downstream dependencies too.

At Line 193 this still only shows task.depends_on. Issue #476 requires both blockers and tasks blocked by the current task, and the current conditional also gives no explicit empty state when neither side exists. Since allTasksData already contains the full workspace task list, this modal has enough data to derive the reverse edges here as well.

🧩 Implementation sketch
   const tasksById = new Map(allTasksData?.tasks.map((t) => [t.id, t]) ?? []);
+  const blockedTasks = task
+    ? (allTasksData?.tasks.filter((candidate) => candidate.depends_on.includes(task.id)) ?? [])
+    : [];

...
-            {task.depends_on.length > 0 && (
-              <div className="space-y-1.5">
+            {(task.depends_on.length > 0 || blockedTasks.length > 0) ? (
+              <div className="space-y-3">
                 ...
+                <div className="flex items-center gap-1.5 text-xs font-medium text-muted-foreground">
+                  <LinkCircleIcon className="h-3.5 w-3.5" />
+                  Blocked by this task ({blockedTasks.length})
+                </div>
+                <ul className="space-y-1">
+                  {blockedTasks.map((blockedTask) => (
+                    <li key={blockedTask.id}>...</li>
+                  ))}
+                </ul>
               </div>
+            ) : (
+              <p className="text-xs italic text-muted-foreground">No dependencies.</p>
             )}

Also applies to: 193-228

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@web-ui/src/components/tasks/TaskDetailModal.tsx` around lines 78 - 84, The
modal currently only renders task.depends_on (upstream blockers) and lacks
downstream dependents and an explicit empty state; use the workspace task list
(allTasksData and tasksById) to compute reverse edges by finding tasks where
t.depends_on includes the current task.id and render them as "blocked by" /
"blocks" sections inside TaskDetailModal (same component that references
tasksById and task.depends_on), showing an explicit empty-state message when
neither upstream nor downstream exist; update the rendering logic around the
block that reads task.depends_on (lines ~193–228) to derive downstreamIds =
allTasksData.tasks.filter(t => t.depends_on?.includes(task.id)).map(t => t.id)
and display corresponding task entries from tasksById for both directions.

useEffect(() => {
if (!open || !taskId) {
setTask(null);
Expand Down Expand Up @@ -172,13 +182,6 @@ export function TaskDetailModal({

{/* Metadata */}
<div className="flex flex-wrap gap-4 text-xs text-muted-foreground">
{task.depends_on.length > 0 && (
<span className="flex items-center gap-1">
<LinkCircleIcon className="h-3.5 w-3.5" />
{task.depends_on.length} dependenc{task.depends_on.length === 1 ? 'y' : 'ies'}:
{' '}{task.depends_on.map((id) => id.slice(0, 8)).join(', ')}
</span>
)}
{task.estimated_hours != null && (
<span className="flex items-center gap-1">
<Time01Icon className="h-3.5 w-3.5" />
Expand All @@ -187,6 +190,43 @@ export function TaskDetailModal({
)}
</div>

{/* Dependencies — full list with status highlights and navigation */}
{task.depends_on.length > 0 && (
<div className="space-y-1.5">
<div className="flex items-center gap-1.5 text-xs font-medium text-muted-foreground">
<LinkCircleIcon className="h-3.5 w-3.5" />
Dependencies ({task.depends_on.length})
</div>
<ul className="space-y-1">
{task.depends_on.map((depId) => {
const dep = tasksById.get(depId);
const isIncomplete = dep && !['DONE', 'MERGED'].includes(dep.status);
return (
<li key={depId} className="flex items-center gap-2 text-xs">
{dep && (
<Badge variant={STATUS_BADGE_VARIANT[dep.status] as never} className="shrink-0 text-[10px]">
{dep.status}
</Badge>
)}
{onOpenTask ? (
<button
className={`truncate text-left hover:underline focus-visible:outline-none focus-visible:ring-[3px] focus-visible:ring-ring ${isIncomplete ? 'font-medium text-amber-700 dark:text-amber-400' : 'text-foreground'}`}
onClick={() => { onClose(); onOpenTask(depId); }}
>
{dep?.title ?? depId.slice(0, 12)}
</button>
) : (
<span className={`truncate ${isIncomplete ? 'font-medium text-amber-700 dark:text-amber-400' : 'text-foreground'}`}>
{dep?.title ?? depId.slice(0, 12)}
</span>
)}
</li>
);
})}
</ul>
</div>
)}
Comment thread
coderabbitai[bot] marked this conversation as resolved.

{/* Requirements */}
{(task.requirement_ids ?? []).length > 0 && (
<div className="space-y-1.5">
Expand Down
Loading