mirror of
https://github.com/AutoMaker-Org/automaker.git
synced 2026-02-02 08:33:36 +00:00
- Introduced a new pipeline service to manage custom workflow steps that execute after a feature is marked "In Progress". - Added API endpoints for configuring, saving, adding, updating, deleting, and reordering pipeline steps. - Enhanced the UI to support pipeline settings, including a dialog for managing steps and integration with the Kanban board. - Updated the application state management to handle pipeline configurations per project. - Implemented dynamic column generation in the Kanban board to display pipeline steps between "In Progress" and "Waiting Approval". - Added documentation for the new pipeline feature, including usage instructions and configuration details. This feature allows for a more structured workflow, enabling automated processes such as code reviews and testing after feature implementation.
264 lines
9.3 KiB
TypeScript
264 lines
9.3 KiB
TypeScript
import { useState, useCallback, useEffect, useRef } from 'react';
|
|
import { useAppStore, Feature } from '@/store/app-store';
|
|
import { getElectronAPI } from '@/lib/electron';
|
|
import { toast } from 'sonner';
|
|
|
|
interface UseBoardFeaturesProps {
|
|
currentProject: { path: string; id: string } | null;
|
|
}
|
|
|
|
export function useBoardFeatures({ currentProject }: UseBoardFeaturesProps) {
|
|
const { features, setFeatures } = useAppStore();
|
|
const [isLoading, setIsLoading] = useState(true);
|
|
const [persistedCategories, setPersistedCategories] = useState<string[]>([]);
|
|
|
|
// Track previous project path to detect project switches
|
|
const prevProjectPathRef = useRef<string | null>(null);
|
|
const isInitialLoadRef = useRef(true);
|
|
const isSwitchingProjectRef = useRef(false);
|
|
|
|
// Load features using features API
|
|
// IMPORTANT: Do NOT add 'features' to dependency array - it would cause infinite reload loop
|
|
const loadFeatures = useCallback(async () => {
|
|
if (!currentProject) return;
|
|
|
|
const currentPath = currentProject.path;
|
|
const previousPath = prevProjectPathRef.current;
|
|
const isProjectSwitch = previousPath !== null && currentPath !== previousPath;
|
|
|
|
// Get cached features from store (without adding to dependencies)
|
|
const cachedFeatures = useAppStore.getState().features;
|
|
|
|
// If project switched, mark it but don't clear features yet
|
|
// We'll clear after successful API load to prevent data loss
|
|
if (isProjectSwitch) {
|
|
console.log(`[BoardView] Project switch detected: ${previousPath} -> ${currentPath}`);
|
|
isSwitchingProjectRef.current = true;
|
|
isInitialLoadRef.current = true;
|
|
}
|
|
|
|
// Update the ref to track current project
|
|
prevProjectPathRef.current = currentPath;
|
|
|
|
// Only show loading spinner on initial load to prevent board flash during reloads
|
|
if (isInitialLoadRef.current) {
|
|
setIsLoading(true);
|
|
}
|
|
|
|
try {
|
|
const api = getElectronAPI();
|
|
if (!api.features) {
|
|
console.error('[BoardView] Features API not available');
|
|
// Keep cached features if API is unavailable
|
|
return;
|
|
}
|
|
|
|
const result = await api.features.getAll(currentProject.path);
|
|
|
|
if (result.success && result.features) {
|
|
const featuresWithIds = result.features.map((f: any, index: number) => ({
|
|
...f,
|
|
id: f.id || `feature-${index}-${Date.now()}`,
|
|
status: f.status || 'backlog',
|
|
startedAt: f.startedAt, // Preserve startedAt timestamp
|
|
// Ensure model and thinkingLevel are set for backward compatibility
|
|
model: f.model || 'opus',
|
|
thinkingLevel: f.thinkingLevel || 'none',
|
|
}));
|
|
// Successfully loaded features - now safe to set them
|
|
setFeatures(featuresWithIds);
|
|
|
|
// Only clear categories on project switch AFTER successful load
|
|
if (isProjectSwitch) {
|
|
setPersistedCategories([]);
|
|
}
|
|
} else if (!result.success && result.error) {
|
|
console.error('[BoardView] API returned error:', result.error);
|
|
// If it's a new project or the error indicates no features found,
|
|
// that's expected - start with empty array
|
|
if (isProjectSwitch) {
|
|
setFeatures([]);
|
|
setPersistedCategories([]);
|
|
}
|
|
// Otherwise keep cached features
|
|
}
|
|
} catch (error) {
|
|
console.error('Failed to load features:', error);
|
|
// On error, keep existing cached features for the current project
|
|
// Only clear on project switch if we have no features from server
|
|
if (isProjectSwitch && cachedFeatures.length === 0) {
|
|
setFeatures([]);
|
|
setPersistedCategories([]);
|
|
}
|
|
} finally {
|
|
setIsLoading(false);
|
|
isInitialLoadRef.current = false;
|
|
isSwitchingProjectRef.current = false;
|
|
}
|
|
}, [currentProject, setFeatures]);
|
|
|
|
// Load persisted categories from file
|
|
const loadCategories = useCallback(async () => {
|
|
if (!currentProject) return;
|
|
|
|
try {
|
|
const api = getElectronAPI();
|
|
const result = await api.readFile(`${currentProject.path}/.automaker/categories.json`);
|
|
|
|
if (result.success && result.content) {
|
|
const parsed = JSON.parse(result.content);
|
|
if (Array.isArray(parsed)) {
|
|
setPersistedCategories(parsed);
|
|
}
|
|
} else {
|
|
// File doesn't exist, ensure categories are cleared
|
|
setPersistedCategories([]);
|
|
}
|
|
} catch (error) {
|
|
console.error('Failed to load categories:', error);
|
|
// If file doesn't exist, ensure categories are cleared
|
|
setPersistedCategories([]);
|
|
}
|
|
}, [currentProject]);
|
|
|
|
// Save a new category to the persisted categories file
|
|
const saveCategory = useCallback(
|
|
async (category: string) => {
|
|
if (!currentProject || !category.trim()) return;
|
|
|
|
try {
|
|
const api = getElectronAPI();
|
|
|
|
// Read existing categories
|
|
let categories: string[] = [...persistedCategories];
|
|
|
|
// Add new category if it doesn't exist
|
|
if (!categories.includes(category)) {
|
|
categories.push(category);
|
|
categories.sort(); // Keep sorted
|
|
|
|
// Write back to file
|
|
await api.writeFile(
|
|
`${currentProject.path}/.automaker/categories.json`,
|
|
JSON.stringify(categories, null, 2)
|
|
);
|
|
|
|
// Update state
|
|
setPersistedCategories(categories);
|
|
}
|
|
} catch (error) {
|
|
console.error('Failed to save category:', error);
|
|
}
|
|
},
|
|
[currentProject, persistedCategories]
|
|
);
|
|
|
|
// Subscribe to spec regeneration complete events to refresh kanban board
|
|
useEffect(() => {
|
|
const api = getElectronAPI();
|
|
if (!api.specRegeneration) return;
|
|
|
|
const unsubscribe = api.specRegeneration.onEvent((event) => {
|
|
// Refresh the kanban board when spec regeneration completes for the current project
|
|
if (
|
|
event.type === 'spec_regeneration_complete' &&
|
|
currentProject &&
|
|
event.projectPath === currentProject.path
|
|
) {
|
|
console.log('[BoardView] Spec regeneration complete, refreshing features');
|
|
loadFeatures();
|
|
}
|
|
});
|
|
|
|
return () => {
|
|
unsubscribe();
|
|
};
|
|
}, [currentProject, loadFeatures]);
|
|
|
|
// Listen for auto mode feature completion and errors to reload features
|
|
useEffect(() => {
|
|
const api = getElectronAPI();
|
|
if (!api?.autoMode || !currentProject) return;
|
|
|
|
const { removeRunningTask } = useAppStore.getState();
|
|
const projectId = currentProject.id;
|
|
|
|
const unsubscribe = api.autoMode.onEvent((event) => {
|
|
// Use event's projectPath or projectId if available, otherwise use current project
|
|
// Board view only reacts to events for the currently selected project
|
|
const eventProjectId = ('projectId' in event && event.projectId) || projectId;
|
|
|
|
if (event.type === 'auto_mode_feature_complete') {
|
|
// Reload features when a feature is completed
|
|
console.log('[Board] Feature completed, reloading features...');
|
|
loadFeatures();
|
|
// Play ding sound when feature is done (unless muted)
|
|
const { muteDoneSound } = useAppStore.getState();
|
|
if (!muteDoneSound) {
|
|
const audio = new Audio('/sounds/ding.mp3');
|
|
audio.play().catch((err) => console.warn('Could not play ding sound:', err));
|
|
}
|
|
} else if (event.type === 'plan_approval_required') {
|
|
// Reload features when plan is generated and requires approval
|
|
// This ensures the feature card shows the "Approve Plan" button
|
|
console.log('[Board] Plan approval required, reloading features...');
|
|
loadFeatures();
|
|
} else if (event.type === 'pipeline_step_started') {
|
|
// Pipeline steps update the feature status to `pipeline_*` before the step runs.
|
|
// Reload so the card moves into the correct pipeline column immediately.
|
|
console.log('[Board] Pipeline step started, reloading features...');
|
|
loadFeatures();
|
|
} else if (event.type === 'auto_mode_error') {
|
|
// Reload features when an error occurs (feature moved to waiting_approval)
|
|
console.log('[Board] Feature error, reloading features...', event.error);
|
|
|
|
// Remove from running tasks so it moves to the correct column
|
|
if (event.featureId) {
|
|
removeRunningTask(eventProjectId, event.featureId);
|
|
}
|
|
|
|
loadFeatures();
|
|
|
|
// Check for authentication errors and show a more helpful message
|
|
const isAuthError =
|
|
event.errorType === 'authentication' ||
|
|
(event.error &&
|
|
(event.error.includes('Authentication failed') ||
|
|
event.error.includes('Invalid API key')));
|
|
|
|
if (isAuthError) {
|
|
toast.error('Authentication Failed', {
|
|
description:
|
|
"Your API key is invalid or expired. Please check Settings or run 'claude login' in terminal.",
|
|
duration: 10000,
|
|
});
|
|
} else {
|
|
toast.error('Agent encountered an error', {
|
|
description: event.error || 'Check the logs for details',
|
|
});
|
|
}
|
|
}
|
|
});
|
|
|
|
return unsubscribe;
|
|
}, [loadFeatures, currentProject]);
|
|
|
|
useEffect(() => {
|
|
loadFeatures();
|
|
}, [loadFeatures]);
|
|
|
|
// Load persisted categories on mount
|
|
useEffect(() => {
|
|
loadCategories();
|
|
}, [loadCategories]);
|
|
|
|
return {
|
|
features,
|
|
isLoading,
|
|
persistedCategories,
|
|
loadFeatures,
|
|
loadCategories,
|
|
saveCategory,
|
|
};
|
|
}
|