mirror of
https://github.com/AutoMaker-Org/automaker.git
synced 2026-02-04 21:23:07 +00:00
refactor(ui): migrate board view to React Query
- Replace manual fetching in use-board-features with useFeatures query - Migrate use-board-actions to use mutation hooks - Update kanban-card and agent-info-panel to use query hooks - Migrate agent-output-modal to useAgentOutput query - Migrate create-pr-dialog to useCreatePR mutation - Remove manual loading/error state management - Add proper cache invalidation on mutations Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -79,6 +79,9 @@ import { SelectionActionBar, ListView } from './board-view/components';
|
|||||||
import { MassEditDialog } from './board-view/dialogs';
|
import { MassEditDialog } from './board-view/dialogs';
|
||||||
import { InitScriptIndicator } from './board-view/init-script-indicator';
|
import { InitScriptIndicator } from './board-view/init-script-indicator';
|
||||||
import { useInitScriptEvents } from '@/hooks/use-init-script-events';
|
import { useInitScriptEvents } from '@/hooks/use-init-script-events';
|
||||||
|
import { usePipelineConfig } from '@/hooks/queries';
|
||||||
|
import { useQueryClient } from '@tanstack/react-query';
|
||||||
|
import { queryKeys } from '@/lib/query-keys';
|
||||||
|
|
||||||
// Stable empty array to avoid infinite loop in selector
|
// Stable empty array to avoid infinite loop in selector
|
||||||
const EMPTY_WORKTREES: ReturnType<ReturnType<typeof useAppStore.getState>['getWorktrees']> = [];
|
const EMPTY_WORKTREES: ReturnType<ReturnType<typeof useAppStore.getState>['getWorktrees']> = [];
|
||||||
@@ -109,8 +112,9 @@ export function BoardView() {
|
|||||||
getPrimaryWorktreeBranch,
|
getPrimaryWorktreeBranch,
|
||||||
setPipelineConfig,
|
setPipelineConfig,
|
||||||
} = useAppStore();
|
} = useAppStore();
|
||||||
// Subscribe to pipelineConfigByProject to trigger re-renders when it changes
|
// Fetch pipeline config via React Query
|
||||||
const pipelineConfigByProject = useAppStore((state) => state.pipelineConfigByProject);
|
const { data: pipelineConfig } = usePipelineConfig(currentProject?.path);
|
||||||
|
const queryClient = useQueryClient();
|
||||||
// Subscribe to worktreePanelVisibleByProject to trigger re-renders when it changes
|
// Subscribe to worktreePanelVisibleByProject to trigger re-renders when it changes
|
||||||
const worktreePanelVisibleByProject = useAppStore((state) => state.worktreePanelVisibleByProject);
|
const worktreePanelVisibleByProject = useAppStore((state) => state.worktreePanelVisibleByProject);
|
||||||
// Subscribe to showInitScriptIndicatorByProject to trigger re-renders when it changes
|
// Subscribe to showInitScriptIndicatorByProject to trigger re-renders when it changes
|
||||||
@@ -241,25 +245,6 @@ export function BoardView() {
|
|||||||
setFeaturesWithContext,
|
setFeaturesWithContext,
|
||||||
});
|
});
|
||||||
|
|
||||||
// Load pipeline config when project changes
|
|
||||||
useEffect(() => {
|
|
||||||
if (!currentProject?.path) return;
|
|
||||||
|
|
||||||
const loadPipelineConfig = async () => {
|
|
||||||
try {
|
|
||||||
const api = getHttpApiClient();
|
|
||||||
const result = await api.pipeline.getConfig(currentProject.path);
|
|
||||||
if (result.success && result.config) {
|
|
||||||
setPipelineConfig(currentProject.path, result.config);
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
logger.error('Failed to load pipeline config:', error);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
loadPipelineConfig();
|
|
||||||
}, [currentProject?.path, setPipelineConfig]);
|
|
||||||
|
|
||||||
// Auto mode hook
|
// Auto mode hook
|
||||||
const autoMode = useAutoMode();
|
const autoMode = useAutoMode();
|
||||||
// Get runningTasks from the hook (scoped to current project)
|
// Get runningTasks from the hook (scoped to current project)
|
||||||
@@ -1131,9 +1116,7 @@ export function BoardView() {
|
|||||||
});
|
});
|
||||||
|
|
||||||
// Build columnFeaturesMap for ListView
|
// Build columnFeaturesMap for ListView
|
||||||
const pipelineConfig = currentProject?.path
|
// pipelineConfig is now from usePipelineConfig React Query hook at the top
|
||||||
? pipelineConfigByProject[currentProject.path] || null
|
|
||||||
: null;
|
|
||||||
const columnFeaturesMap = useMemo(() => {
|
const columnFeaturesMap = useMemo(() => {
|
||||||
const columns = getColumnsWithPipeline(pipelineConfig);
|
const columns = getColumnsWithPipeline(pipelineConfig);
|
||||||
const map: Record<string, typeof hookFeatures> = {};
|
const map: Record<string, typeof hookFeatures> = {};
|
||||||
@@ -1585,6 +1568,11 @@ export function BoardView() {
|
|||||||
if (!result.success) {
|
if (!result.success) {
|
||||||
throw new Error(result.error || 'Failed to save pipeline config');
|
throw new Error(result.error || 'Failed to save pipeline config');
|
||||||
}
|
}
|
||||||
|
// Invalidate React Query cache to refetch updated config
|
||||||
|
queryClient.invalidateQueries({
|
||||||
|
queryKey: queryKeys.pipeline.config(currentProject.path),
|
||||||
|
});
|
||||||
|
// Also update Zustand for backward compatibility
|
||||||
setPipelineConfig(currentProject.path, config);
|
setPipelineConfig(currentProject.path, config);
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
// @ts-nocheck
|
|
||||||
import { useEffect, useState, useMemo } from 'react';
|
import { useEffect, useState, useMemo } from 'react';
|
||||||
import { Feature, ThinkingLevel, ParsedTask } from '@/store/app-store';
|
import { Feature, ThinkingLevel, ParsedTask } from '@/store/app-store';
|
||||||
import type { ReasoningEffort } from '@automaker/types';
|
import type { ReasoningEffort } from '@automaker/types';
|
||||||
@@ -24,6 +23,7 @@ import {
|
|||||||
import { getElectronAPI } from '@/lib/electron';
|
import { getElectronAPI } from '@/lib/electron';
|
||||||
import { SummaryDialog } from './summary-dialog';
|
import { SummaryDialog } from './summary-dialog';
|
||||||
import { getProviderIconForModel } from '@/components/ui/provider-icon';
|
import { getProviderIconForModel } from '@/components/ui/provider-icon';
|
||||||
|
import { useFeature, useAgentOutput } from '@/hooks/queries';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Formats thinking level for compact display
|
* Formats thinking level for compact display
|
||||||
@@ -58,6 +58,7 @@ function formatReasoningEffort(effort: ReasoningEffort | undefined): string {
|
|||||||
|
|
||||||
interface AgentInfoPanelProps {
|
interface AgentInfoPanelProps {
|
||||||
feature: Feature;
|
feature: Feature;
|
||||||
|
projectPath: string;
|
||||||
contextContent?: string;
|
contextContent?: string;
|
||||||
summary?: string;
|
summary?: string;
|
||||||
isCurrentAutoTask?: boolean;
|
isCurrentAutoTask?: boolean;
|
||||||
@@ -65,23 +66,54 @@ interface AgentInfoPanelProps {
|
|||||||
|
|
||||||
export function AgentInfoPanel({
|
export function AgentInfoPanel({
|
||||||
feature,
|
feature,
|
||||||
|
projectPath,
|
||||||
contextContent,
|
contextContent,
|
||||||
summary,
|
summary,
|
||||||
isCurrentAutoTask,
|
isCurrentAutoTask,
|
||||||
}: AgentInfoPanelProps) {
|
}: AgentInfoPanelProps) {
|
||||||
const [agentInfo, setAgentInfo] = useState<AgentTaskInfo | null>(null);
|
|
||||||
const [isSummaryDialogOpen, setIsSummaryDialogOpen] = useState(false);
|
const [isSummaryDialogOpen, setIsSummaryDialogOpen] = useState(false);
|
||||||
const [isTodosExpanded, setIsTodosExpanded] = useState(false);
|
const [isTodosExpanded, setIsTodosExpanded] = useState(false);
|
||||||
// Track real-time task status updates from WebSocket events
|
// Track real-time task status updates from WebSocket events
|
||||||
const [taskStatusMap, setTaskStatusMap] = useState<
|
const [taskStatusMap, setTaskStatusMap] = useState<
|
||||||
Map<string, 'pending' | 'in_progress' | 'completed'>
|
Map<string, 'pending' | 'in_progress' | 'completed'>
|
||||||
>(new Map());
|
>(new Map());
|
||||||
// Fresh planSpec data fetched from API (store data is stale for task progress)
|
|
||||||
const [freshPlanSpec, setFreshPlanSpec] = useState<{
|
// Determine if we should poll for updates
|
||||||
tasks?: ParsedTask[];
|
const shouldPoll = isCurrentAutoTask || feature.status === 'in_progress';
|
||||||
tasksCompleted?: number;
|
const shouldFetchData = feature.status !== 'backlog';
|
||||||
currentTaskId?: string;
|
|
||||||
} | null>(null);
|
// Fetch fresh feature data for planSpec (store data can be stale for task progress)
|
||||||
|
const { data: freshFeature } = useFeature(projectPath, feature.id, {
|
||||||
|
enabled: shouldFetchData && !contextContent,
|
||||||
|
pollingInterval: shouldPoll ? 3000 : false,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Fetch agent output for parsing
|
||||||
|
const { data: agentOutputContent } = useAgentOutput(projectPath, feature.id, {
|
||||||
|
enabled: shouldFetchData && !contextContent,
|
||||||
|
pollingInterval: shouldPoll ? 3000 : false,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Parse agent output into agentInfo
|
||||||
|
const agentInfo = useMemo(() => {
|
||||||
|
if (contextContent) {
|
||||||
|
return parseAgentContext(contextContent);
|
||||||
|
}
|
||||||
|
if (agentOutputContent) {
|
||||||
|
return parseAgentContext(agentOutputContent);
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}, [contextContent, agentOutputContent]);
|
||||||
|
|
||||||
|
// Fresh planSpec data from API (more accurate than store data for task progress)
|
||||||
|
const freshPlanSpec = useMemo(() => {
|
||||||
|
if (!freshFeature?.planSpec) return null;
|
||||||
|
return {
|
||||||
|
tasks: freshFeature.planSpec.tasks,
|
||||||
|
tasksCompleted: freshFeature.planSpec.tasksCompleted || 0,
|
||||||
|
currentTaskId: freshFeature.planSpec.currentTaskId,
|
||||||
|
};
|
||||||
|
}, [freshFeature?.planSpec]);
|
||||||
|
|
||||||
// Derive effective todos from planSpec.tasks when available, fallback to agentInfo.todos
|
// Derive effective todos from planSpec.tasks when available, fallback to agentInfo.todos
|
||||||
// Uses freshPlanSpec (from API) for accurate progress, with taskStatusMap for real-time updates
|
// Uses freshPlanSpec (from API) for accurate progress, with taskStatusMap for real-time updates
|
||||||
@@ -133,73 +165,6 @@ export function AgentInfoPanel({
|
|||||||
taskStatusMap,
|
taskStatusMap,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
const loadContext = async () => {
|
|
||||||
if (contextContent) {
|
|
||||||
const info = parseAgentContext(contextContent);
|
|
||||||
setAgentInfo(info);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (feature.status === 'backlog') {
|
|
||||||
setAgentInfo(null);
|
|
||||||
setFreshPlanSpec(null);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
const api = getElectronAPI();
|
|
||||||
const currentProject = (window as any).__currentProject;
|
|
||||||
if (!currentProject?.path) return;
|
|
||||||
|
|
||||||
if (api.features) {
|
|
||||||
// Fetch fresh feature data to get up-to-date planSpec (store data is stale)
|
|
||||||
try {
|
|
||||||
const featureResult = await api.features.get(currentProject.path, feature.id);
|
|
||||||
const freshFeature: any = (featureResult as any).feature;
|
|
||||||
if (featureResult.success && freshFeature?.planSpec) {
|
|
||||||
setFreshPlanSpec({
|
|
||||||
tasks: freshFeature.planSpec.tasks,
|
|
||||||
tasksCompleted: freshFeature.planSpec.tasksCompleted || 0,
|
|
||||||
currentTaskId: freshFeature.planSpec.currentTaskId,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
} catch {
|
|
||||||
// Ignore errors fetching fresh planSpec
|
|
||||||
}
|
|
||||||
|
|
||||||
const result = await api.features.getAgentOutput(currentProject.path, feature.id);
|
|
||||||
|
|
||||||
if (result.success && result.content) {
|
|
||||||
const info = parseAgentContext(result.content);
|
|
||||||
setAgentInfo(info);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
const contextPath = `${currentProject.path}/.automaker/features/${feature.id}/agent-output.md`;
|
|
||||||
const result = await api.readFile(contextPath);
|
|
||||||
|
|
||||||
if (result.success && result.content) {
|
|
||||||
const info = parseAgentContext(result.content);
|
|
||||||
setAgentInfo(info);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch {
|
|
||||||
console.debug('[KanbanCard] No context file for feature:', feature.id);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
loadContext();
|
|
||||||
|
|
||||||
// Poll for updates when feature is in_progress (not just isCurrentAutoTask)
|
|
||||||
// This ensures planSpec progress stays in sync
|
|
||||||
if (isCurrentAutoTask || feature.status === 'in_progress') {
|
|
||||||
const interval = setInterval(loadContext, 3000);
|
|
||||||
return () => {
|
|
||||||
clearInterval(interval);
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}, [feature.id, feature.status, contextContent, isCurrentAutoTask]);
|
|
||||||
|
|
||||||
// Listen to WebSocket events for real-time task status updates
|
// Listen to WebSocket events for real-time task status updates
|
||||||
// This ensures the Kanban card shows the same progress as the Agent Output modal
|
// This ensures the Kanban card shows the same progress as the Agent Output modal
|
||||||
// Listen for ANY in-progress feature with planSpec tasks, not just isCurrentAutoTask
|
// Listen for ANY in-progress feature with planSpec tasks, not just isCurrentAutoTask
|
||||||
|
|||||||
@@ -97,7 +97,7 @@ export const KanbanCard = memo(function KanbanCard({
|
|||||||
isSelected = false,
|
isSelected = false,
|
||||||
onToggleSelect,
|
onToggleSelect,
|
||||||
}: KanbanCardProps) {
|
}: KanbanCardProps) {
|
||||||
const { useWorktrees } = useAppStore();
|
const { useWorktrees, currentProject } = useAppStore();
|
||||||
const [isLifted, setIsLifted] = useState(false);
|
const [isLifted, setIsLifted] = useState(false);
|
||||||
|
|
||||||
useLayoutEffect(() => {
|
useLayoutEffect(() => {
|
||||||
@@ -213,6 +213,7 @@ export const KanbanCard = memo(function KanbanCard({
|
|||||||
{/* Agent Info Panel */}
|
{/* Agent Info Panel */}
|
||||||
<AgentInfoPanel
|
<AgentInfoPanel
|
||||||
feature={feature}
|
feature={feature}
|
||||||
|
projectPath={currentProject?.path ?? ''}
|
||||||
contextContent={contextContent}
|
contextContent={contextContent}
|
||||||
summary={summary}
|
summary={summary}
|
||||||
isCurrentAutoTask={isCurrentAutoTask}
|
isCurrentAutoTask={isCurrentAutoTask}
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ import { TaskProgressPanel } from '@/components/ui/task-progress-panel';
|
|||||||
import { Markdown } from '@/components/ui/markdown';
|
import { Markdown } from '@/components/ui/markdown';
|
||||||
import { useAppStore } from '@/store/app-store';
|
import { useAppStore } from '@/store/app-store';
|
||||||
import { extractSummary } from '@/lib/log-parser';
|
import { extractSummary } from '@/lib/log-parser';
|
||||||
|
import { useAgentOutput } from '@/hooks/queries';
|
||||||
import type { AutoModeEvent } from '@/types/electron';
|
import type { AutoModeEvent } from '@/types/electron';
|
||||||
|
|
||||||
interface AgentOutputModalProps {
|
interface AgentOutputModalProps {
|
||||||
@@ -40,10 +41,29 @@ export function AgentOutputModal({
|
|||||||
onNumberKeyPress,
|
onNumberKeyPress,
|
||||||
projectPath: projectPathProp,
|
projectPath: projectPathProp,
|
||||||
}: AgentOutputModalProps) {
|
}: AgentOutputModalProps) {
|
||||||
const [output, setOutput] = useState<string>('');
|
// Resolve project path - prefer prop, fallback to window.__currentProject
|
||||||
const [isLoading, setIsLoading] = useState(true);
|
const resolvedProjectPath = projectPathProp || (window as any).__currentProject?.path || '';
|
||||||
|
|
||||||
|
// Track additional content from WebSocket events (appended to query data)
|
||||||
|
const [streamedContent, setStreamedContent] = useState<string>('');
|
||||||
const [viewMode, setViewMode] = useState<ViewMode | null>(null);
|
const [viewMode, setViewMode] = useState<ViewMode | null>(null);
|
||||||
const [projectPath, setProjectPath] = useState<string>('');
|
|
||||||
|
// Use React Query for initial output loading
|
||||||
|
const { data: initialOutput = '', isLoading } = useAgentOutput(
|
||||||
|
resolvedProjectPath,
|
||||||
|
featureId,
|
||||||
|
open && !!resolvedProjectPath
|
||||||
|
);
|
||||||
|
|
||||||
|
// Reset streamed content when modal opens or featureId changes
|
||||||
|
useEffect(() => {
|
||||||
|
if (open) {
|
||||||
|
setStreamedContent('');
|
||||||
|
}
|
||||||
|
}, [open, featureId]);
|
||||||
|
|
||||||
|
// Combine initial output from query with streamed content from WebSocket
|
||||||
|
const output = initialOutput + streamedContent;
|
||||||
|
|
||||||
// Extract summary from output
|
// Extract summary from output
|
||||||
const summary = useMemo(() => extractSummary(output), [output]);
|
const summary = useMemo(() => extractSummary(output), [output]);
|
||||||
@@ -52,7 +72,6 @@ export function AgentOutputModal({
|
|||||||
const effectiveViewMode = viewMode ?? (summary ? 'summary' : 'parsed');
|
const effectiveViewMode = viewMode ?? (summary ? 'summary' : 'parsed');
|
||||||
const scrollRef = useRef<HTMLDivElement>(null);
|
const scrollRef = useRef<HTMLDivElement>(null);
|
||||||
const autoScrollRef = useRef(true);
|
const autoScrollRef = useRef(true);
|
||||||
const projectPathRef = useRef<string>('');
|
|
||||||
const useWorktrees = useAppStore((state) => state.useWorktrees);
|
const useWorktrees = useAppStore((state) => state.useWorktrees);
|
||||||
|
|
||||||
// Auto-scroll to bottom when output changes
|
// Auto-scroll to bottom when output changes
|
||||||
@@ -62,50 +81,6 @@ export function AgentOutputModal({
|
|||||||
}
|
}
|
||||||
}, [output]);
|
}, [output]);
|
||||||
|
|
||||||
// Load existing output from file
|
|
||||||
useEffect(() => {
|
|
||||||
if (!open) return;
|
|
||||||
|
|
||||||
const loadOutput = async () => {
|
|
||||||
const api = getElectronAPI();
|
|
||||||
if (!api) return;
|
|
||||||
|
|
||||||
setIsLoading(true);
|
|
||||||
|
|
||||||
try {
|
|
||||||
// Use projectPath prop if provided, otherwise fall back to window.__currentProject for backward compatibility
|
|
||||||
const resolvedProjectPath = projectPathProp || (window as any).__currentProject?.path;
|
|
||||||
if (!resolvedProjectPath) {
|
|
||||||
setIsLoading(false);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
projectPathRef.current = resolvedProjectPath;
|
|
||||||
setProjectPath(resolvedProjectPath);
|
|
||||||
|
|
||||||
// Use features API to get agent output
|
|
||||||
if (api.features) {
|
|
||||||
const result = await api.features.getAgentOutput(resolvedProjectPath, featureId);
|
|
||||||
|
|
||||||
if (result.success) {
|
|
||||||
setOutput(result.content || '');
|
|
||||||
} else {
|
|
||||||
setOutput('');
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
setOutput('');
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Failed to load output:', error);
|
|
||||||
setOutput('');
|
|
||||||
} finally {
|
|
||||||
setIsLoading(false);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
loadOutput();
|
|
||||||
}, [open, featureId, projectPathProp]);
|
|
||||||
|
|
||||||
// Listen to auto mode events and update output
|
// Listen to auto mode events and update output
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!open) return;
|
if (!open) return;
|
||||||
@@ -264,8 +239,8 @@ export function AgentOutputModal({
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (newContent) {
|
if (newContent) {
|
||||||
// Only update local state - server is the single source of truth for file writes
|
// Append new content from WebSocket to streamed content
|
||||||
setOutput((prev) => prev + newContent);
|
setStreamedContent((prev) => prev + newContent);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -379,15 +354,15 @@ export function AgentOutputModal({
|
|||||||
{/* Task Progress Panel - shows when tasks are being executed */}
|
{/* Task Progress Panel - shows when tasks are being executed */}
|
||||||
<TaskProgressPanel
|
<TaskProgressPanel
|
||||||
featureId={featureId}
|
featureId={featureId}
|
||||||
projectPath={projectPath}
|
projectPath={resolvedProjectPath}
|
||||||
className="flex-shrink-0 mx-3 my-2"
|
className="flex-shrink-0 mx-3 my-2"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{effectiveViewMode === 'changes' ? (
|
{effectiveViewMode === 'changes' ? (
|
||||||
<div className="flex-1 min-h-0 sm:min-h-[200px] sm:max-h-[60vh] overflow-y-auto scrollbar-visible">
|
<div className="flex-1 min-h-0 sm:min-h-[200px] sm:max-h-[60vh] overflow-y-auto scrollbar-visible">
|
||||||
{projectPath ? (
|
{resolvedProjectPath ? (
|
||||||
<GitDiffPanel
|
<GitDiffPanel
|
||||||
projectPath={projectPath}
|
projectPath={resolvedProjectPath}
|
||||||
featureId={featureId}
|
featureId={featureId}
|
||||||
compact={false}
|
compact={false}
|
||||||
useWorktrees={useWorktrees}
|
useWorktrees={useWorktrees}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { useState, useEffect, useRef, useCallback } from 'react';
|
import { useState, useEffect, useRef, useCallback, useMemo } from 'react';
|
||||||
import {
|
import {
|
||||||
Dialog,
|
Dialog,
|
||||||
DialogContent,
|
DialogContent,
|
||||||
@@ -16,6 +16,7 @@ import { BranchAutocomplete } from '@/components/ui/branch-autocomplete';
|
|||||||
import { GitPullRequest, Loader2, ExternalLink } from 'lucide-react';
|
import { GitPullRequest, Loader2, ExternalLink } from 'lucide-react';
|
||||||
import { getElectronAPI } from '@/lib/electron';
|
import { getElectronAPI } from '@/lib/electron';
|
||||||
import { toast } from 'sonner';
|
import { toast } from 'sonner';
|
||||||
|
import { useWorktreeBranches } from '@/hooks/queries';
|
||||||
|
|
||||||
interface WorktreeInfo {
|
interface WorktreeInfo {
|
||||||
path: string;
|
path: string;
|
||||||
@@ -53,12 +54,21 @@ export function CreatePRDialog({
|
|||||||
const [prUrl, setPrUrl] = useState<string | null>(null);
|
const [prUrl, setPrUrl] = useState<string | null>(null);
|
||||||
const [browserUrl, setBrowserUrl] = useState<string | null>(null);
|
const [browserUrl, setBrowserUrl] = useState<string | null>(null);
|
||||||
const [showBrowserFallback, setShowBrowserFallback] = useState(false);
|
const [showBrowserFallback, setShowBrowserFallback] = useState(false);
|
||||||
// Branch fetching state
|
|
||||||
const [branches, setBranches] = useState<string[]>([]);
|
|
||||||
const [isLoadingBranches, setIsLoadingBranches] = useState(false);
|
|
||||||
// Track whether an operation completed that warrants a refresh
|
// Track whether an operation completed that warrants a refresh
|
||||||
const operationCompletedRef = useRef(false);
|
const operationCompletedRef = useRef(false);
|
||||||
|
|
||||||
|
// Use React Query for branch fetching - only enabled when dialog is open
|
||||||
|
const { data: branchesData, isLoading: isLoadingBranches } = useWorktreeBranches(
|
||||||
|
open ? worktree?.path : undefined,
|
||||||
|
true // Include remote branches for PR base branch selection
|
||||||
|
);
|
||||||
|
|
||||||
|
// Filter out current worktree branch from the list
|
||||||
|
const branches = useMemo(() => {
|
||||||
|
if (!branchesData?.branches) return [];
|
||||||
|
return branchesData.branches.map((b) => b.name).filter((name) => name !== worktree?.branch);
|
||||||
|
}, [branchesData?.branches, worktree?.branch]);
|
||||||
|
|
||||||
// Common state reset function to avoid duplication
|
// Common state reset function to avoid duplication
|
||||||
const resetState = useCallback(() => {
|
const resetState = useCallback(() => {
|
||||||
setTitle('');
|
setTitle('');
|
||||||
@@ -71,44 +81,13 @@ export function CreatePRDialog({
|
|||||||
setBrowserUrl(null);
|
setBrowserUrl(null);
|
||||||
setShowBrowserFallback(false);
|
setShowBrowserFallback(false);
|
||||||
operationCompletedRef.current = false;
|
operationCompletedRef.current = false;
|
||||||
setBranches([]);
|
|
||||||
}, [defaultBaseBranch]);
|
}, [defaultBaseBranch]);
|
||||||
|
|
||||||
// Fetch branches for autocomplete
|
|
||||||
const fetchBranches = useCallback(async () => {
|
|
||||||
if (!worktree?.path) return;
|
|
||||||
|
|
||||||
setIsLoadingBranches(true);
|
|
||||||
try {
|
|
||||||
const api = getElectronAPI();
|
|
||||||
if (!api?.worktree?.listBranches) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
// Fetch both local and remote branches for PR base branch selection
|
|
||||||
const result = await api.worktree.listBranches(worktree.path, true);
|
|
||||||
if (result.success && result.result) {
|
|
||||||
// Extract branch names, filtering out the current worktree branch
|
|
||||||
const branchNames = result.result.branches
|
|
||||||
.map((b) => b.name)
|
|
||||||
.filter((name) => name !== worktree.branch);
|
|
||||||
setBranches(branchNames);
|
|
||||||
}
|
|
||||||
} catch {
|
|
||||||
// Silently fail - branches will default to main only
|
|
||||||
} finally {
|
|
||||||
setIsLoadingBranches(false);
|
|
||||||
}
|
|
||||||
}, [worktree?.path, worktree?.branch]);
|
|
||||||
|
|
||||||
// Reset state when dialog opens or worktree changes
|
// Reset state when dialog opens or worktree changes
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
// Reset all state on both open and close
|
// Reset all state on both open and close
|
||||||
resetState();
|
resetState();
|
||||||
if (open) {
|
}, [open, worktree?.path, resetState]);
|
||||||
// Fetch fresh branches when dialog opens
|
|
||||||
fetchBranches();
|
|
||||||
}
|
|
||||||
}, [open, worktree?.path, resetState, fetchBranches]);
|
|
||||||
|
|
||||||
const handleCreate = async () => {
|
const handleCreate = async () => {
|
||||||
if (!worktree) return;
|
if (!worktree) return;
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ import { getElectronAPI } from '@/lib/electron';
|
|||||||
import { isConnectionError, handleServerOffline } from '@/lib/http-api-client';
|
import { isConnectionError, handleServerOffline } from '@/lib/http-api-client';
|
||||||
import { toast } from 'sonner';
|
import { toast } from 'sonner';
|
||||||
import { useAutoMode } from '@/hooks/use-auto-mode';
|
import { useAutoMode } from '@/hooks/use-auto-mode';
|
||||||
|
import { useVerifyFeature, useResumeFeature } from '@/hooks/mutations';
|
||||||
import { truncateDescription } from '@/lib/utils';
|
import { truncateDescription } from '@/lib/utils';
|
||||||
import { getBlockingDependencies } from '@automaker/dependency-resolver';
|
import { getBlockingDependencies } from '@automaker/dependency-resolver';
|
||||||
import { createLogger } from '@automaker/utils/logger';
|
import { createLogger } from '@automaker/utils/logger';
|
||||||
@@ -94,6 +95,10 @@ export function useBoardActions({
|
|||||||
} = useAppStore();
|
} = useAppStore();
|
||||||
const autoMode = useAutoMode();
|
const autoMode = useAutoMode();
|
||||||
|
|
||||||
|
// React Query mutations for feature operations
|
||||||
|
const verifyFeatureMutation = useVerifyFeature(currentProject?.path ?? '');
|
||||||
|
const resumeFeatureMutation = useResumeFeature(currentProject?.path ?? '');
|
||||||
|
|
||||||
// Worktrees are created when adding/editing features with a branch name
|
// Worktrees are created when adding/editing features with a branch name
|
||||||
// This ensures the worktree exists before the feature starts execution
|
// This ensures the worktree exists before the feature starts execution
|
||||||
|
|
||||||
@@ -480,28 +485,9 @@ export function useBoardActions({
|
|||||||
const handleVerifyFeature = useCallback(
|
const handleVerifyFeature = useCallback(
|
||||||
async (feature: Feature) => {
|
async (feature: Feature) => {
|
||||||
if (!currentProject) return;
|
if (!currentProject) return;
|
||||||
|
verifyFeatureMutation.mutate(feature.id);
|
||||||
try {
|
|
||||||
const api = getElectronAPI();
|
|
||||||
if (!api?.autoMode) {
|
|
||||||
logger.error('Auto mode API not available');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const result = await api.autoMode.verifyFeature(currentProject.path, feature.id);
|
|
||||||
|
|
||||||
if (result.success) {
|
|
||||||
logger.info('Feature verification started successfully');
|
|
||||||
} else {
|
|
||||||
logger.error('Failed to verify feature:', result.error);
|
|
||||||
await loadFeatures();
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
logger.error('Error verifying feature:', error);
|
|
||||||
await loadFeatures();
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
[currentProject, loadFeatures]
|
[currentProject, verifyFeatureMutation]
|
||||||
);
|
);
|
||||||
|
|
||||||
const handleResumeFeature = useCallback(
|
const handleResumeFeature = useCallback(
|
||||||
@@ -511,40 +497,9 @@ export function useBoardActions({
|
|||||||
logger.error('No current project');
|
logger.error('No current project');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
resumeFeatureMutation.mutate({ featureId: feature.id, useWorktrees });
|
||||||
try {
|
|
||||||
const api = getElectronAPI();
|
|
||||||
if (!api?.autoMode) {
|
|
||||||
logger.error('Auto mode API not available');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
logger.info('Calling resumeFeature API...', {
|
|
||||||
projectPath: currentProject.path,
|
|
||||||
featureId: feature.id,
|
|
||||||
useWorktrees,
|
|
||||||
});
|
|
||||||
|
|
||||||
const result = await api.autoMode.resumeFeature(
|
|
||||||
currentProject.path,
|
|
||||||
feature.id,
|
|
||||||
useWorktrees
|
|
||||||
);
|
|
||||||
|
|
||||||
logger.info('resumeFeature result:', result);
|
|
||||||
|
|
||||||
if (result.success) {
|
|
||||||
logger.info('Feature resume started successfully');
|
|
||||||
} else {
|
|
||||||
logger.error('Failed to resume feature:', result.error);
|
|
||||||
await loadFeatures();
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
logger.error('Error resuming feature:', error);
|
|
||||||
await loadFeatures();
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
[currentProject, loadFeatures, useWorktrees]
|
[currentProject, resumeFeatureMutation, useWorktrees]
|
||||||
);
|
);
|
||||||
|
|
||||||
const handleManualVerify = useCallback(
|
const handleManualVerify = useCallback(
|
||||||
|
|||||||
@@ -1,8 +1,18 @@
|
|||||||
import { useState, useCallback, useEffect, useRef } from 'react';
|
/**
|
||||||
import { useAppStore, Feature } from '@/store/app-store';
|
* 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 { getElectronAPI } from '@/lib/electron';
|
||||||
import { toast } from 'sonner';
|
import { toast } from 'sonner';
|
||||||
import { createLogger } from '@automaker/utils/logger';
|
import { createLogger } from '@automaker/utils/logger';
|
||||||
|
import { useFeatures } from '@/hooks/queries';
|
||||||
|
import { queryKeys } from '@/lib/query-keys';
|
||||||
|
|
||||||
const logger = createLogger('BoardFeatures');
|
const logger = createLogger('BoardFeatures');
|
||||||
|
|
||||||
@@ -11,105 +21,15 @@ interface UseBoardFeaturesProps {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function useBoardFeatures({ currentProject }: UseBoardFeaturesProps) {
|
export function useBoardFeatures({ currentProject }: UseBoardFeaturesProps) {
|
||||||
const { features, setFeatures } = useAppStore();
|
const queryClient = useQueryClient();
|
||||||
const [isLoading, setIsLoading] = useState(true);
|
|
||||||
const [persistedCategories, setPersistedCategories] = useState<string[]>([]);
|
const [persistedCategories, setPersistedCategories] = useState<string[]>([]);
|
||||||
|
|
||||||
// Track previous project path to detect project switches
|
// Use React Query for features
|
||||||
const prevProjectPathRef = useRef<string | null>(null);
|
const {
|
||||||
const isInitialLoadRef = useRef(true);
|
data: features = [],
|
||||||
const isSwitchingProjectRef = useRef(false);
|
isLoading,
|
||||||
|
refetch: loadFeatures,
|
||||||
// Load features using features API
|
} = useFeatures(currentProject?.path);
|
||||||
// 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) {
|
|
||||||
logger.info(`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) {
|
|
||||||
logger.error('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([]);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check for interrupted features and resume them
|
|
||||||
// This handles server restarts where features were in pipeline steps
|
|
||||||
if (api.autoMode?.resumeInterrupted) {
|
|
||||||
try {
|
|
||||||
await api.autoMode.resumeInterrupted(currentProject.path);
|
|
||||||
logger.info('Checked for interrupted features');
|
|
||||||
} catch (resumeError) {
|
|
||||||
logger.warn('Failed to check for interrupted features:', resumeError);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else if (!result.success && result.error) {
|
|
||||||
logger.error('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) {
|
|
||||||
logger.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
|
// Load persisted categories from file
|
||||||
const loadCategories = useCallback(async () => {
|
const loadCategories = useCallback(async () => {
|
||||||
@@ -125,12 +45,9 @@ export function useBoardFeatures({ currentProject }: UseBoardFeaturesProps) {
|
|||||||
setPersistedCategories(parsed);
|
setPersistedCategories(parsed);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// File doesn't exist, ensure categories are cleared
|
|
||||||
setPersistedCategories([]);
|
setPersistedCategories([]);
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch {
|
||||||
logger.error('Failed to load categories:', error);
|
|
||||||
// If file doesn't exist, ensure categories are cleared
|
|
||||||
setPersistedCategories([]);
|
setPersistedCategories([]);
|
||||||
}
|
}
|
||||||
}, [currentProject]);
|
}, [currentProject]);
|
||||||
@@ -142,22 +59,17 @@ export function useBoardFeatures({ currentProject }: UseBoardFeaturesProps) {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
const api = getElectronAPI();
|
const api = getElectronAPI();
|
||||||
|
|
||||||
// Read existing categories
|
|
||||||
let categories: string[] = [...persistedCategories];
|
let categories: string[] = [...persistedCategories];
|
||||||
|
|
||||||
// Add new category if it doesn't exist
|
|
||||||
if (!categories.includes(category)) {
|
if (!categories.includes(category)) {
|
||||||
categories.push(category);
|
categories.push(category);
|
||||||
categories.sort(); // Keep sorted
|
categories.sort();
|
||||||
|
|
||||||
// Write back to file
|
|
||||||
await api.writeFile(
|
await api.writeFile(
|
||||||
`${currentProject.path}/.automaker/categories.json`,
|
`${currentProject.path}/.automaker/categories.json`,
|
||||||
JSON.stringify(categories, null, 2)
|
JSON.stringify(categories, null, 2)
|
||||||
);
|
);
|
||||||
|
|
||||||
// Update state
|
|
||||||
setPersistedCategories(categories);
|
setPersistedCategories(categories);
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -167,29 +79,8 @@ export function useBoardFeatures({ currentProject }: UseBoardFeaturesProps) {
|
|||||||
[currentProject, persistedCategories]
|
[currentProject, persistedCategories]
|
||||||
);
|
);
|
||||||
|
|
||||||
// Subscribe to spec regeneration complete events to refresh kanban board
|
// Subscribe to auto mode events for notifications (ding sound, toasts)
|
||||||
useEffect(() => {
|
// Note: Query invalidation is handled by useAutoModeQueryInvalidation in the root
|
||||||
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
|
|
||||||
) {
|
|
||||||
logger.info('Spec regeneration complete, refreshing features');
|
|
||||||
loadFeatures();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
return () => {
|
|
||||||
unsubscribe();
|
|
||||||
};
|
|
||||||
}, [currentProject, loadFeatures]);
|
|
||||||
|
|
||||||
// Listen for auto mode feature completion and errors to reload features
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const api = getElectronAPI();
|
const api = getElectronAPI();
|
||||||
if (!api?.autoMode || !currentProject) return;
|
if (!api?.autoMode || !currentProject) return;
|
||||||
@@ -198,42 +89,22 @@ export function useBoardFeatures({ currentProject }: UseBoardFeaturesProps) {
|
|||||||
const projectId = currentProject.id;
|
const projectId = currentProject.id;
|
||||||
|
|
||||||
const unsubscribe = api.autoMode.onEvent((event) => {
|
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;
|
const eventProjectId = ('projectId' in event && event.projectId) || projectId;
|
||||||
|
|
||||||
if (event.type === 'auto_mode_feature_complete') {
|
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)
|
// Play ding sound when feature is done (unless muted)
|
||||||
const { muteDoneSound } = useAppStore.getState();
|
const { muteDoneSound } = useAppStore.getState();
|
||||||
if (!muteDoneSound) {
|
if (!muteDoneSound) {
|
||||||
const audio = new Audio('/sounds/ding.mp3');
|
const audio = new Audio('/sounds/ding.mp3');
|
||||||
audio.play().catch((err) => logger.warn('Could not play ding sound:', err));
|
audio.play().catch((err) => logger.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
|
|
||||||
logger.info('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.
|
|
||||||
logger.info('Pipeline step started, reloading features...');
|
|
||||||
loadFeatures();
|
|
||||||
} else if (event.type === 'auto_mode_error') {
|
} else if (event.type === 'auto_mode_error') {
|
||||||
// Reload features when an error occurs (feature moved to waiting_approval)
|
// Remove from running tasks
|
||||||
logger.info('Feature error, reloading features...', event.error);
|
|
||||||
|
|
||||||
// Remove from running tasks so it moves to the correct column
|
|
||||||
if (event.featureId) {
|
if (event.featureId) {
|
||||||
removeRunningTask(eventProjectId, event.featureId);
|
removeRunningTask(eventProjectId, event.featureId);
|
||||||
}
|
}
|
||||||
|
|
||||||
loadFeatures();
|
// Show error toast
|
||||||
|
|
||||||
// Check for authentication errors and show a more helpful message
|
|
||||||
const isAuthError =
|
const isAuthError =
|
||||||
event.errorType === 'authentication' ||
|
event.errorType === 'authentication' ||
|
||||||
(event.error &&
|
(event.error &&
|
||||||
@@ -255,22 +126,46 @@ export function useBoardFeatures({ currentProject }: UseBoardFeaturesProps) {
|
|||||||
});
|
});
|
||||||
|
|
||||||
return unsubscribe;
|
return unsubscribe;
|
||||||
}, [loadFeatures, currentProject]);
|
}, [currentProject]);
|
||||||
|
|
||||||
|
// Check for interrupted features on mount
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
loadFeatures();
|
if (!currentProject) return;
|
||||||
}, [loadFeatures]);
|
|
||||||
|
|
||||||
// Load persisted categories on mount
|
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(() => {
|
useEffect(() => {
|
||||||
loadCategories();
|
loadCategories();
|
||||||
}, [loadCategories]);
|
}, [loadCategories]);
|
||||||
|
|
||||||
|
// Clear categories when project changes
|
||||||
|
useEffect(() => {
|
||||||
|
setPersistedCategories([]);
|
||||||
|
}, [currentProject?.path]);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
features,
|
features,
|
||||||
isLoading,
|
isLoading,
|
||||||
persistedCategories,
|
persistedCategories,
|
||||||
loadFeatures,
|
loadFeatures: () => {
|
||||||
|
queryClient.invalidateQueries({
|
||||||
|
queryKey: queryKeys.features.all(currentProject?.path ?? ''),
|
||||||
|
});
|
||||||
|
},
|
||||||
loadCategories,
|
loadCategories,
|
||||||
saveCategory,
|
saveCategory,
|
||||||
};
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user