mirror of
https://github.com/AutoMaker-Org/automaker.git
synced 2026-01-31 06:42:03 +00:00
- Extended SetupAPI interface with 20+ missing methods for Cursor, Codex, OpenCode, Gemini, and Copilot CLI integrations - Fixed WorktreeInfo type to include isCurrent and hasWorktree fields - Added null checks for optional API properties across all hooks - Fixed Feature type conflicts between @automaker/types and local definitions - Added missing CLI status hooks for all providers - Fixed type mismatches in mutation callbacks and event handlers - Removed dead code referencing non-existent GlobalSettings properties - Updated mock implementations in electron.ts for all new API methods Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
197 lines
6.5 KiB
TypeScript
197 lines
6.5 KiB
TypeScript
/**
|
|
* Board Features Hook
|
|
*
|
|
* React Query-based hook for managing features on the board view.
|
|
* Handles feature loading, categories, and auto-mode event notifications.
|
|
*/
|
|
|
|
import { useState, useCallback, useEffect } from 'react';
|
|
import { useQueryClient } from '@tanstack/react-query';
|
|
import { useAppStore } from '@/store/app-store';
|
|
import { getElectronAPI } from '@/lib/electron';
|
|
import { toast } from 'sonner';
|
|
import { createLogger } from '@automaker/utils/logger';
|
|
import { useFeatures } from '@/hooks/queries';
|
|
import { queryKeys } from '@/lib/query-keys';
|
|
|
|
const logger = createLogger('BoardFeatures');
|
|
|
|
interface UseBoardFeaturesProps {
|
|
currentProject: { path: string; id: string } | null;
|
|
}
|
|
|
|
export function useBoardFeatures({ currentProject }: UseBoardFeaturesProps) {
|
|
const queryClient = useQueryClient();
|
|
const [persistedCategories, setPersistedCategories] = useState<string[]>([]);
|
|
|
|
// Use React Query for features
|
|
const {
|
|
data: features = [],
|
|
isLoading,
|
|
refetch: loadFeatures,
|
|
} = useFeatures(currentProject?.path);
|
|
|
|
// 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 {
|
|
setPersistedCategories([]);
|
|
}
|
|
} catch {
|
|
setPersistedCategories([]);
|
|
}
|
|
}, [currentProject, loadFeatures]);
|
|
|
|
// Save a new category to the persisted categories file
|
|
const saveCategory = useCallback(
|
|
async (category: string) => {
|
|
if (!currentProject || !category.trim()) return;
|
|
|
|
try {
|
|
const api = getElectronAPI();
|
|
let categories: string[] = [...persistedCategories];
|
|
|
|
if (!categories.includes(category)) {
|
|
categories.push(category);
|
|
categories.sort();
|
|
|
|
await api.writeFile(
|
|
`${currentProject.path}/.automaker/categories.json`,
|
|
JSON.stringify(categories, null, 2)
|
|
);
|
|
|
|
setPersistedCategories(categories);
|
|
}
|
|
} catch (error) {
|
|
logger.error('Failed to save category:', error);
|
|
}
|
|
},
|
|
[currentProject, persistedCategories]
|
|
);
|
|
|
|
// Subscribe to auto mode events for notifications (ding sound, toasts)
|
|
// Note: Query invalidation is handled by useAutoModeQueryInvalidation in the root
|
|
useEffect(() => {
|
|
const api = getElectronAPI();
|
|
if (!api?.autoMode || !currentProject) return;
|
|
|
|
const { removeRunningTask } = useAppStore.getState();
|
|
const projectId = currentProject.id;
|
|
const projectPath = currentProject.path;
|
|
|
|
const unsubscribe = api.autoMode.onEvent((event) => {
|
|
// Check if event is for the current project by matching projectPath
|
|
const eventProjectPath = ('projectPath' in event && event.projectPath) as string | undefined;
|
|
if (eventProjectPath && eventProjectPath !== projectPath) {
|
|
// Event is for a different project, ignore it
|
|
logger.debug(
|
|
`Ignoring auto mode event for different project: ${eventProjectPath} (current: ${projectPath})`
|
|
);
|
|
return;
|
|
}
|
|
|
|
// 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_start') {
|
|
// Reload features when a feature starts to ensure status update (backlog -> in_progress) is reflected
|
|
logger.info(
|
|
`[BoardFeatures] Feature ${event.featureId} started for project ${projectPath}, reloading features to update status...`
|
|
);
|
|
loadFeatures();
|
|
} else if (event.type === 'auto_mode_feature_complete') {
|
|
// Reload features when a feature is completed
|
|
logger.info('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) => logger.warn('Could not play ding sound:', err));
|
|
}
|
|
} else if (event.type === 'auto_mode_error') {
|
|
// Remove from running tasks
|
|
if (event.featureId) {
|
|
const eventBranchName =
|
|
'branchName' in event && event.branchName !== undefined ? event.branchName : null;
|
|
removeRunningTask(eventProjectId, eventBranchName, event.featureId);
|
|
}
|
|
|
|
// Show error toast
|
|
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;
|
|
}, [currentProject]);
|
|
|
|
// Check for interrupted features on mount
|
|
useEffect(() => {
|
|
if (!currentProject) return;
|
|
|
|
const checkInterrupted = async () => {
|
|
const api = getElectronAPI();
|
|
if (api.autoMode?.resumeInterrupted) {
|
|
try {
|
|
await api.autoMode.resumeInterrupted(currentProject.path);
|
|
logger.info('Checked for interrupted features');
|
|
} catch (error) {
|
|
logger.warn('Failed to check for interrupted features:', error);
|
|
}
|
|
}
|
|
};
|
|
|
|
checkInterrupted();
|
|
}, [currentProject]);
|
|
|
|
// Load persisted categories on mount/project change
|
|
useEffect(() => {
|
|
loadCategories();
|
|
}, [loadCategories]);
|
|
|
|
// Clear categories when project changes
|
|
useEffect(() => {
|
|
setPersistedCategories([]);
|
|
}, [currentProject?.path]);
|
|
|
|
return {
|
|
features,
|
|
isLoading,
|
|
persistedCategories,
|
|
loadFeatures: async () => {
|
|
await queryClient.invalidateQueries({
|
|
queryKey: queryKeys.features.all(currentProject?.path ?? ''),
|
|
});
|
|
},
|
|
loadCategories,
|
|
saveCategory,
|
|
};
|
|
}
|