mirror of
https://github.com/AutoMaker-Org/automaker.git
synced 2026-01-30 06:12:03 +00:00
add node actions
This commit is contained in:
@@ -1040,6 +1040,9 @@ export function BoardView() {
|
||||
onSearchQueryChange={setSearchQuery}
|
||||
onEditFeature={(feature) => setEditingFeature(feature)}
|
||||
onViewOutput={handleViewOutput}
|
||||
onStartTask={handleStartImplementation}
|
||||
onStopTask={handleForceStopFeature}
|
||||
onResumeTask={handleResumeFeature}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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"
|
||||
/>
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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 };
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user