Files
automaker/apps/ui/src/components/views/graph-view/graph-view.tsx
2026-01-19 19:38:56 +05:30

246 lines
7.8 KiB
TypeScript

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';
import { wouldCreateCircularDependency, dependencyExists } from '@automaker/dependency-resolver';
import { toast } from 'sonner';
interface GraphViewProps {
features: Feature[];
runningAutoTasks: string[];
currentWorktreePath: string | null;
currentWorktreeBranch: string | null;
projectPath: string | null;
searchQuery: string;
onSearchQueryChange: (query: string) => void;
onEditFeature: (feature: Feature) => void;
onViewOutput: (feature: Feature) => void;
onStartTask?: (feature: Feature) => void;
onStopTask?: (feature: Feature) => void;
onResumeTask?: (feature: Feature) => void;
onUpdateFeature?: (featureId: string, updates: Partial<Feature>) => void;
onSpawnTask?: (feature: Feature) => void;
onDeleteTask?: (feature: Feature) => void;
onAddFeature?: () => void;
onOpenPlanDialog?: () => void;
hasPendingPlan?: boolean;
planUseSelectedWorktreeBranch?: boolean;
onPlanUseSelectedWorktreeBranchChange?: (value: boolean) => void;
}
export function GraphView({
features,
runningAutoTasks,
currentWorktreePath,
currentWorktreeBranch,
projectPath,
searchQuery,
onSearchQueryChange,
onEditFeature,
onViewOutput,
onStartTask,
onStopTask,
onResumeTask,
onUpdateFeature,
onSpawnTask,
onDeleteTask,
onAddFeature,
onOpenPlanDialog,
hasPendingPlan,
planUseSelectedWorktreeBranch,
onPlanUseSelectedWorktreeBranchChange,
}: GraphViewProps) {
const currentProject = useAppStore((state) => state.currentProject);
// Use the same background hook as the board view
const { backgroundImageStyle, backgroundSettings } = useBoardBackground({ currentProject });
// Filter features by current worktree (same logic as board view)
const filteredFeatures = useMemo(() => {
const effectiveBranch = currentWorktreeBranch;
return features.filter((f) => {
const featureBranch = f.branchName as string | undefined;
if (!featureBranch) {
// No branch assigned - show only on primary worktree
return currentWorktreePath === null;
} else if (effectiveBranch === null) {
// Viewing main but branch not initialized
return projectPath
? useAppStore.getState().isPrimaryWorktreeBranch(projectPath, featureBranch)
: false;
} else {
// Match by branch name
return featureBranch === effectiveBranch;
}
});
}, [features, currentWorktreePath, currentWorktreeBranch, projectPath]);
// Handle node double click - edit
const handleNodeDoubleClick = useCallback(
(featureId: string) => {
const feature = features.find((f) => f.id === featureId);
if (feature) {
onEditFeature(feature);
}
},
[features, onEditFeature]
);
// Handle creating a dependency via edge connection
const handleCreateDependency = useCallback(
async (sourceId: string, targetId: string): Promise<boolean> => {
const targetFeature = features.find((f) => f.id === targetId);
// Prevent self-dependency
if (sourceId === targetId) {
toast.error('A task cannot depend on itself');
return false;
}
// Check if dependency already exists
if (dependencyExists(features, sourceId, targetId)) {
toast.info('Dependency already exists');
return false;
}
// Check for circular dependency
// This checks: if we make targetId depend on sourceId, would it create a cycle?
// A cycle would occur if sourceId already depends on targetId (transitively)
const wouldCycle = wouldCreateCircularDependency(features, sourceId, targetId);
if (wouldCycle) {
toast.error('Cannot create circular dependency', {
description: 'This would create a dependency cycle',
});
return false;
}
// Get target feature and update its dependencies
if (!targetFeature) {
toast.error('Target task not found');
return false;
}
const currentDeps = (targetFeature.dependencies as string[] | undefined) || [];
// Add the dependency
onUpdateFeature?.(targetId, {
dependencies: [...currentDeps, sourceId],
});
toast.success('Dependency created');
return true;
},
[features, onUpdateFeature]
);
// 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);
}
},
onViewDetails: (featureId: string) => {
const feature = features.find((f) => f.id === featureId);
if (feature) {
onEditFeature(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);
}
},
onSpawnTask: (featureId: string) => {
const feature = features.find((f) => f.id === featureId);
if (feature) {
onSpawnTask?.(feature);
}
},
onDeleteTask: (featureId: string) => {
const feature = features.find((f) => f.id === featureId);
if (feature) {
onDeleteTask?.(feature);
}
},
onDeleteDependency: (sourceId: string, targetId: string) => {
// Find the target feature and remove the source from its dependencies
console.log('onDeleteDependency called', { sourceId, targetId });
const targetFeature = features.find((f) => f.id === targetId);
if (!targetFeature) {
console.error('Target feature not found:', targetId);
return;
}
const currentDeps = (targetFeature.dependencies as string[] | undefined) || [];
console.log('Current dependencies:', currentDeps);
const newDeps = currentDeps.filter((depId) => depId !== sourceId);
console.log('New dependencies:', newDeps);
if (onUpdateFeature) {
console.log('Calling onUpdateFeature');
onUpdateFeature(targetId, {
dependencies: newDeps,
});
} else {
console.error('onUpdateFeature is not defined!');
}
toast.success('Dependency removed');
},
}),
[
features,
onViewOutput,
onEditFeature,
onStartTask,
onStopTask,
onResumeTask,
onSpawnTask,
onDeleteTask,
onUpdateFeature,
]
);
return (
<div className="flex-1 overflow-hidden relative">
<GraphCanvas
features={filteredFeatures}
runningAutoTasks={runningAutoTasks}
searchQuery={searchQuery}
onSearchQueryChange={onSearchQueryChange}
onNodeDoubleClick={handleNodeDoubleClick}
nodeActionCallbacks={nodeActionCallbacks}
onCreateDependency={handleCreateDependency}
onAddFeature={onAddFeature}
onOpenPlanDialog={onOpenPlanDialog}
hasPendingPlan={hasPendingPlan}
planUseSelectedWorktreeBranch={planUseSelectedWorktreeBranch}
onPlanUseSelectedWorktreeBranchChange={onPlanUseSelectedWorktreeBranchChange}
backgroundStyle={backgroundImageStyle}
backgroundSettings={backgroundSettings}
projectPath={projectPath}
className="h-full"
/>
</div>
);
}