add node actions

This commit is contained in:
James
2025-12-22 18:53:44 -05:00
parent 12a796bcbb
commit b3c321ce02
6 changed files with 198 additions and 37 deletions

View File

@@ -1040,6 +1040,9 @@ export function BoardView() {
onSearchQueryChange={setSearchQuery}
onEditFeature={(feature) => setEditingFeature(feature)}
onViewOutput={handleViewOutput}
onStartTask={handleStartImplementation}
onStopTask={handleForceStopFeature}
onResumeTask={handleResumeFeature}
/>
)}
</div>

View File

@@ -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
</TooltipProvider>
)}
{/* Stopped indicator - task is in_progress but not actively running */}
{isStopped && (
<TooltipProvider delayDuration={200}>
<Tooltip>
<TooltipTrigger asChild>
<div className="p-1 rounded bg-[var(--status-warning-bg)]">
<Pause className="w-3 h-3 text-[var(--status-warning)]" />
</div>
</TooltipTrigger>
<TooltipContent side="top" className="text-xs max-w-[200px]">
<p>Task paused - click menu to resume</p>
</TooltipContent>
</Tooltip>
</TooltipProvider>
)}
{/* Actions dropdown */}
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant="ghost" size="sm" className="h-6 w-6 p-0 hover:bg-background/50">
<MoreHorizontal className="w-4 h-4" />
<Button
variant="ghost"
size="sm"
className={cn(
'h-7 w-7 p-0 rounded-md',
'bg-background/60 hover:bg-background',
'border border-border/50 hover:border-border',
'shadow-sm',
'transition-all duration-150'
)}
onClick={(e) => e.stopPropagation()}
>
<MoreVertical className="w-4 h-4 text-foreground" />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end" className="w-40">
<DropdownMenuItem className="text-xs">
<DropdownMenuContent
align="end"
className="w-44"
onClick={(e) => e.stopPropagation()}
>
<DropdownMenuItem
className="text-xs cursor-pointer"
onClick={(e) => {
e.stopPropagation();
data.onViewLogs?.();
}}
>
<Terminal className="w-3 h-3 mr-2" />
View Agent Logs
</DropdownMenuItem>
<DropdownMenuItem
className="text-xs cursor-pointer"
onClick={(e) => {
e.stopPropagation();
data.onViewLogs?.();
}}
>
<Eye className="w-3 h-3 mr-2" />
View Details
</DropdownMenuItem>
{data.status === 'backlog' && !data.isBlocked && (
<DropdownMenuItem className="text-xs">
<DropdownMenuItem
className="text-xs cursor-pointer"
onClick={(e) => {
e.stopPropagation();
data.onStartTask?.();
}}
>
<Play className="w-3 h-3 mr-2" />
Start Task
</DropdownMenuItem>
)}
{data.isRunning && (
<DropdownMenuItem className="text-xs text-[var(--status-error)]">
<DropdownMenuItem
className="text-xs text-[var(--status-error)] cursor-pointer"
onClick={(e) => {
e.stopPropagation();
data.onStopTask?.();
}}
>
<Pause className="w-3 h-3 mr-2" />
Stop Task
</DropdownMenuItem>
)}
<DropdownMenuSeparator />
<DropdownMenuItem className="text-xs">
<GitBranch className="w-3 h-3 mr-2" />
View Branch
</DropdownMenuItem>
{isStopped && (
<DropdownMenuItem
className="text-xs text-[var(--status-success)] cursor-pointer"
onClick={(e) => {
e.stopPropagation();
data.onResumeTask?.();
}}
>
<RotateCcw className="w-3 h-3 mr-2" />
Resume Task
</DropdownMenuItem>
)}
{Boolean(data.branchName) && <DropdownMenuSeparator />}
{Boolean(data.branchName) && (
<DropdownMenuItem
className="text-xs cursor-pointer"
onClick={(e) => {
e.stopPropagation();
data.onViewBranch?.();
}}
>
<GitBranch className="w-3 h-3 mr-2" />
View Branch
</DropdownMenuItem>
)}
</DropdownMenuContent>
</DropdownMenu>
</div>
@@ -223,6 +307,16 @@ export const TaskNode = memo(function TaskNode({ data, selected }: TaskNodeProps
</div>
)}
{/* Paused indicator for stopped tasks */}
{isStopped && (
<div className="mt-2 flex items-center gap-2">
<div className="flex-1 h-1.5 bg-muted rounded-full overflow-hidden">
<div className="h-full w-1/2 bg-[var(--status-warning)] rounded-full" />
</div>
<span className="text-[10px] text-[var(--status-warning)] font-medium">Paused</span>
</div>
)}
{/* Branch name if assigned */}
{data.branchName && (
<div className="mt-2 flex items-center gap-1 text-[10px] text-muted-foreground">

View File

@@ -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<TaskNodeData>) => {
onNodeClick?.(node.id);
},
[onNodeClick]
);
// Handle node double click
const handleNodeDoubleClick = useCallback(
(_event: React.MouseEvent, node: Node<TaskNodeData>) => {
@@ -160,7 +154,6 @@ function GraphCanvasInner({
edges={edges}
onNodesChange={isLocked ? undefined : onNodesChange}
onEdgesChange={onEdgesChange}
onNodeClick={handleNodeClick}
onNodeDoubleClick={handleNodeDoubleClick}
nodeTypes={nodeTypes}
edgeTypes={edgeTypes}

View File

@@ -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 (
<div className="flex-1 overflow-hidden relative">
<GraphCanvas
@@ -85,8 +119,8 @@ export function GraphView({
runningAutoTasks={runningAutoTasks}
searchQuery={searchQuery}
onSearchQueryChange={onSearchQueryChange}
onNodeClick={handleNodeClick}
onNodeDoubleClick={handleNodeDoubleClick}
nodeActionCallbacks={nodeActionCallbacks}
backgroundStyle={backgroundImageStyle}
className="h-full"
/>

View File

@@ -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';

View File

@@ -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<TaskNodeData, 'task'>;
@@ -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 };
}