diff --git a/apps/ui/src/components/views/board-view.tsx b/apps/ui/src/components/views/board-view.tsx index 700595cd..0459e68b 100644 --- a/apps/ui/src/components/views/board-view.tsx +++ b/apps/ui/src/components/views/board-view.tsx @@ -1040,6 +1040,9 @@ export function BoardView() { onSearchQueryChange={setSearchQuery} onEditFeature={(feature) => setEditingFeature(feature)} onViewOutput={handleViewOutput} + onStartTask={handleStartImplementation} + onStopTask={handleForceStopFeature} + onResumeTask={handleResumeFeature} /> )} diff --git a/apps/ui/src/components/views/graph-view/components/task-node.tsx b/apps/ui/src/components/views/graph-view/components/task-node.tsx index 3696a3d5..44118662 100644 --- a/apps/ui/src/components/views/graph-view/components/task-node.tsx +++ b/apps/ui/src/components/views/graph-view/components/task-node.tsx @@ -10,8 +10,10 @@ import { Play, Pause, Eye, - MoreHorizontal, + MoreVertical, GitBranch, + Terminal, + RotateCcw, } from 'lucide-react'; import { TaskNodeData } from '../hooks/use-graph-nodes'; import { Button } from '@/components/ui/button'; @@ -82,6 +84,9 @@ export const TaskNode = memo(function TaskNode({ data, selected }: TaskNodeProps const isHighlighted = data.isHighlighted ?? false; const isDimmed = data.isDimmed ?? false; + // Task is stopped if it's in_progress but not actively running + const isStopped = data.status === 'in_progress' && !data.isRunning; + return ( <> {/* Target handle (left side - receives dependencies) */} @@ -167,35 +172,114 @@ export const TaskNode = memo(function TaskNode({ data, selected }: TaskNodeProps )} + {/* Stopped indicator - task is in_progress but not actively running */} + {isStopped && ( + + + +
+ +
+
+ +

Task paused - click menu to resume

+
+
+
+ )} + {/* Actions dropdown */} - - - + e.stopPropagation()} + > + { + e.stopPropagation(); + data.onViewLogs?.(); + }} + > + + View Agent Logs + + { + e.stopPropagation(); + data.onViewLogs?.(); + }} + > View Details {data.status === 'backlog' && !data.isBlocked && ( - + { + e.stopPropagation(); + data.onStartTask?.(); + }} + > Start Task )} {data.isRunning && ( - + { + e.stopPropagation(); + data.onStopTask?.(); + }} + > Stop Task )} - - - - View Branch - + {isStopped && ( + { + e.stopPropagation(); + data.onResumeTask?.(); + }} + > + + Resume Task + + )} + {Boolean(data.branchName) && } + {Boolean(data.branchName) && ( + { + e.stopPropagation(); + data.onViewBranch?.(); + }} + > + + View Branch + + )} @@ -223,6 +307,16 @@ export const TaskNode = memo(function TaskNode({ data, selected }: TaskNodeProps )} + {/* Paused indicator for stopped tasks */} + {isStopped && ( +
+
+
+
+ Paused +
+ )} + {/* Branch name if assigned */} {data.branchName && (
diff --git a/apps/ui/src/components/views/graph-view/graph-canvas.tsx b/apps/ui/src/components/views/graph-view/graph-canvas.tsx index 40b5b487..ca9a4017 100644 --- a/apps/ui/src/components/views/graph-view/graph-canvas.tsx +++ b/apps/ui/src/components/views/graph-view/graph-canvas.tsx @@ -27,6 +27,7 @@ import { useGraphFilter, type TaskNodeData, type GraphFilterState, + type NodeActionCallbacks, } from './hooks'; import { cn } from '@/lib/utils'; @@ -46,8 +47,8 @@ interface GraphCanvasProps { runningAutoTasks: string[]; searchQuery: string; onSearchQueryChange: (query: string) => void; - onNodeClick?: (featureId: string) => void; onNodeDoubleClick?: (featureId: string) => void; + nodeActionCallbacks?: NodeActionCallbacks; backgroundStyle?: React.CSSProperties; className?: string; } @@ -57,8 +58,8 @@ function GraphCanvasInner({ runningAutoTasks, searchQuery, onSearchQueryChange, - onNodeClick, onNodeDoubleClick, + nodeActionCallbacks, backgroundStyle, className, }: GraphCanvasProps) { @@ -84,6 +85,7 @@ function GraphCanvasInner({ features, runningAutoTasks, filterResult, + actionCallbacks: nodeActionCallbacks, }); // Apply layout @@ -118,14 +120,6 @@ function GraphCanvasInner({ setIsNegativeFilter(false); }, [onSearchQueryChange]); - // Handle node click - const handleNodeClick = useCallback( - (_event: React.MouseEvent, node: Node) => { - onNodeClick?.(node.id); - }, - [onNodeClick] - ); - // Handle node double click const handleNodeDoubleClick = useCallback( (_event: React.MouseEvent, node: Node) => { @@ -160,7 +154,6 @@ function GraphCanvasInner({ edges={edges} onNodesChange={isLocked ? undefined : onNodesChange} onEdgesChange={onEdgesChange} - onNodeClick={handleNodeClick} onNodeDoubleClick={handleNodeDoubleClick} nodeTypes={nodeTypes} edgeTypes={edgeTypes} diff --git a/apps/ui/src/components/views/graph-view/graph-view.tsx b/apps/ui/src/components/views/graph-view/graph-view.tsx index 61b2db14..1f054c3a 100644 --- a/apps/ui/src/components/views/graph-view/graph-view.tsx +++ b/apps/ui/src/components/views/graph-view/graph-view.tsx @@ -2,6 +2,7 @@ import { useMemo, useCallback } from 'react'; import { Feature, useAppStore } from '@/store/app-store'; import { GraphCanvas } from './graph-canvas'; import { useBoardBackground } from '../board-view/hooks'; +import { NodeActionCallbacks } from './hooks'; interface GraphViewProps { features: Feature[]; @@ -13,6 +14,9 @@ interface GraphViewProps { onSearchQueryChange: (query: string) => void; onEditFeature: (feature: Feature) => void; onViewOutput: (feature: Feature) => void; + onStartTask?: (feature: Feature) => void; + onStopTask?: (feature: Feature) => void; + onResumeTask?: (feature: Feature) => void; } export function GraphView({ @@ -25,6 +29,9 @@ export function GraphView({ onSearchQueryChange, onEditFeature, onViewOutput, + onStartTask, + onStopTask, + onResumeTask, }: GraphViewProps) { const { currentProject } = useAppStore(); @@ -56,17 +63,6 @@ export function GraphView({ }); }, [features, currentWorktreePath, currentWorktreeBranch, projectPath]); - // Handle node click - view details - const handleNodeClick = useCallback( - (featureId: string) => { - const feature = features.find((f) => f.id === featureId); - if (feature) { - onViewOutput(feature); - } - }, - [features, onViewOutput] - ); - // Handle node double click - edit const handleNodeDoubleClick = useCallback( (featureId: string) => { @@ -78,6 +74,44 @@ export function GraphView({ [features, onEditFeature] ); + // Node action callbacks for dropdown menu + const nodeActionCallbacks: NodeActionCallbacks = useMemo( + () => ({ + onViewLogs: (featureId: string) => { + const feature = features.find((f) => f.id === featureId); + if (feature) { + onViewOutput(feature); + } + }, + onStartTask: (featureId: string) => { + const feature = features.find((f) => f.id === featureId); + if (feature) { + onStartTask?.(feature); + } + }, + onStopTask: (featureId: string) => { + const feature = features.find((f) => f.id === featureId); + if (feature) { + onStopTask?.(feature); + } + }, + onResumeTask: (featureId: string) => { + const feature = features.find((f) => f.id === featureId); + if (feature) { + onResumeTask?.(feature); + } + }, + onViewBranch: (featureId: string) => { + const feature = features.find((f) => f.id === featureId); + if (feature?.branchName) { + // TODO: Implement view branch action + console.log('View branch:', feature.branchName); + } + }, + }), + [features, onViewOutput, onStartTask, onStopTask, onResumeTask] + ); + return (
diff --git a/apps/ui/src/components/views/graph-view/hooks/index.ts b/apps/ui/src/components/views/graph-view/hooks/index.ts index fd08d803..579f583e 100644 --- a/apps/ui/src/components/views/graph-view/hooks/index.ts +++ b/apps/ui/src/components/views/graph-view/hooks/index.ts @@ -3,6 +3,7 @@ export { type TaskNode, type DependencyEdge, type TaskNodeData, + type NodeActionCallbacks, } from './use-graph-nodes'; export { useGraphLayout } from './use-graph-layout'; export { useGraphFilter, type GraphFilterState, type GraphFilterResult } from './use-graph-filter'; diff --git a/apps/ui/src/components/views/graph-view/hooks/use-graph-nodes.ts b/apps/ui/src/components/views/graph-view/hooks/use-graph-nodes.ts index 308bbe2e..5dc38b83 100644 --- a/apps/ui/src/components/views/graph-view/hooks/use-graph-nodes.ts +++ b/apps/ui/src/components/views/graph-view/hooks/use-graph-nodes.ts @@ -12,6 +12,12 @@ export interface TaskNodeData extends Feature { isMatched?: boolean; isHighlighted?: boolean; isDimmed?: boolean; + // Action callbacks + onViewLogs?: () => void; + onStartTask?: () => void; + onStopTask?: () => void; + onResumeTask?: () => void; + onViewBranch?: () => void; } export type TaskNode = Node; @@ -22,17 +28,31 @@ export type DependencyEdge = Edge<{ isDimmed?: boolean; }>; +export interface NodeActionCallbacks { + onViewLogs?: (featureId: string) => void; + onStartTask?: (featureId: string) => void; + onStopTask?: (featureId: string) => void; + onResumeTask?: (featureId: string) => void; + onViewBranch?: (featureId: string) => void; +} + interface UseGraphNodesProps { features: Feature[]; runningAutoTasks: string[]; filterResult?: GraphFilterResult; + actionCallbacks?: NodeActionCallbacks; } /** * Transforms features into React Flow nodes and edges * Creates dependency edges based on feature.dependencies array */ -export function useGraphNodes({ features, runningAutoTasks, filterResult }: UseGraphNodesProps) { +export function useGraphNodes({ + features, + runningAutoTasks, + filterResult, + actionCallbacks, +}: UseGraphNodesProps) { const { nodes, edges } = useMemo(() => { const nodeList: TaskNode[] = []; const edgeList: DependencyEdge[] = []; @@ -70,6 +90,22 @@ export function useGraphNodes({ features, runningAutoTasks, filterResult }: UseG isMatched, isHighlighted, isDimmed, + // Action callbacks (bound to this feature's ID) + onViewLogs: actionCallbacks?.onViewLogs + ? () => actionCallbacks.onViewLogs!(feature.id) + : undefined, + onStartTask: actionCallbacks?.onStartTask + ? () => actionCallbacks.onStartTask!(feature.id) + : undefined, + onStopTask: actionCallbacks?.onStopTask + ? () => actionCallbacks.onStopTask!(feature.id) + : undefined, + onResumeTask: actionCallbacks?.onResumeTask + ? () => actionCallbacks.onResumeTask!(feature.id) + : undefined, + onViewBranch: actionCallbacks?.onViewBranch + ? () => actionCallbacks.onViewBranch!(feature.id) + : undefined, }, }; @@ -107,7 +143,7 @@ export function useGraphNodes({ features, runningAutoTasks, filterResult }: UseG }); return { nodes: nodeList, edges: edgeList }; - }, [features, runningAutoTasks, filterResult]); + }, [features, runningAutoTasks, filterResult, actionCallbacks]); return { nodes, edges }; }