Files
automaker/apps/ui/src/components/views/board-view/hooks/use-board-features.ts
Shirone 5c335641fa chore: Fix all 246 TypeScript errors in UI
- 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>
2026-01-25 18:36:47 +01:00

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,
};
}