refactor(ui): migrate remaining components to React Query

- Migrate workspace-picker-modal to useWorkspaceDirectories query
- Migrate session-manager to useSessions query
- Migrate git-diff-panel to useGitDiffs query
- Migrate prompt-list to useIdeationPrompts query
- Migrate spec-view hooks to useSpecFile query and spec mutations
- Migrate use-board-background-settings to useProjectSettings query
- Migrate use-guided-prompts to useIdeationPrompts query
- Migrate use-project-settings-loader to React Query
- Complete React Query migration across all components

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
Shirone
2026-01-15 16:22:39 +01:00
parent 5fe7bcd378
commit c2fed78733
10 changed files with 308 additions and 434 deletions

View File

@@ -8,7 +8,7 @@ import { Card, CardContent } from '@/components/ui/card';
import { useGuidedPrompts } from '@/hooks/use-guided-prompts';
import { useIdeationStore } from '@/store/ideation-store';
import { useAppStore } from '@/store/app-store';
import { getElectronAPI } from '@/lib/electron';
import { useGenerateIdeationSuggestions } from '@/hooks/mutations';
import { toast } from 'sonner';
import { useNavigate } from '@tanstack/react-router';
import type { IdeaCategory, IdeationPrompt } from '@automaker/types';
@@ -27,6 +27,9 @@ export function PromptList({ category, onBack }: PromptListProps) {
const [loadingPromptId, setLoadingPromptId] = useState<string | null>(null);
const [startedPrompts, setStartedPrompts] = useState<Set<string>>(new Set());
const navigate = useNavigate();
// React Query mutation
const generateMutation = useGenerateIdeationSuggestions(currentProject?.path ?? '');
const {
getPromptsByCategory,
isLoading: isLoadingPrompts,
@@ -56,7 +59,7 @@ export function PromptList({ category, onBack }: PromptListProps) {
return;
}
if (loadingPromptId || generatingPromptIds.has(prompt.id)) return;
if (loadingPromptId || generateMutation.isPending || generatingPromptIds.has(prompt.id)) return;
setLoadingPromptId(prompt.id);
@@ -68,42 +71,31 @@ export function PromptList({ category, onBack }: PromptListProps) {
toast.info(`Generating ideas for "${prompt.title}"...`);
setMode('dashboard');
try {
const api = getElectronAPI();
const result = await api.ideation?.generateSuggestions(
currentProject.path,
prompt.id,
category
);
if (result?.success && result.suggestions) {
updateJobStatus(jobId, 'ready', result.suggestions);
toast.success(`Generated ${result.suggestions.length} ideas for "${prompt.title}"`, {
duration: 10000,
action: {
label: 'View Ideas',
onClick: () => {
setMode('dashboard');
navigate({ to: '/ideation' });
generateMutation.mutate(
{ promptId: prompt.id, category },
{
onSuccess: (data) => {
updateJobStatus(jobId, 'ready', data.suggestions);
toast.success(`Generated ${data.suggestions.length} ideas for "${prompt.title}"`, {
duration: 10000,
action: {
label: 'View Ideas',
onClick: () => {
setMode('dashboard');
navigate({ to: '/ideation' });
},
},
},
});
} else {
updateJobStatus(
jobId,
'error',
undefined,
result?.error || 'Failed to generate suggestions'
);
toast.error(result?.error || 'Failed to generate suggestions');
});
setLoadingPromptId(null);
},
onError: (error) => {
console.error('Failed to generate suggestions:', error);
updateJobStatus(jobId, 'error', undefined, error.message);
toast.error(error.message);
setLoadingPromptId(null);
},
}
} catch (error) {
console.error('Failed to generate suggestions:', error);
updateJobStatus(jobId, 'error', undefined, (error as Error).message);
toast.error((error as Error).message);
} finally {
setLoadingPromptId(null);
}
);
};
return (

View File

@@ -10,6 +10,7 @@ import { createElement } from 'react';
import { SPEC_FILE_WRITE_DELAY, STATUS_CHECK_INTERVAL_MS } from '../constants';
import type { FeatureCount } from '../types';
import type { SpecRegenerationEvent } from '@/types/electron';
import { useCreateSpec, useRegenerateSpec, useGenerateFeatures } from '@/hooks/mutations';
interface UseSpecGenerationOptions {
loadSpec: () => Promise<void>;
@@ -18,6 +19,11 @@ interface UseSpecGenerationOptions {
export function useSpecGeneration({ loadSpec }: UseSpecGenerationOptions) {
const { currentProject } = useAppStore();
// React Query mutations
const createSpecMutation = useCreateSpec(currentProject?.path ?? '');
const regenerateSpecMutation = useRegenerateSpec(currentProject?.path ?? '');
const generateFeaturesMutation = useGenerateFeatures(currentProject?.path ?? '');
// Dialog visibility state
const [showCreateDialog, setShowCreateDialog] = useState(false);
const [showRegenerateDialog, setShowRegenerateDialog] = useState(false);
@@ -404,47 +410,34 @@ export function useSpecGeneration({ loadSpec }: UseSpecGenerationOptions) {
logsRef.current = '';
setLogs('');
logger.debug('[useSpecGeneration] Starting spec creation, generateFeatures:', generateFeatures);
try {
const api = getElectronAPI();
if (!api.specRegeneration) {
logger.error('[useSpecGeneration] Spec regeneration not available');
setIsCreating(false);
return;
}
const result = await api.specRegeneration.create(
currentProject.path,
projectOverview.trim(),
generateFeatures,
analyzeProjectOnCreate,
generateFeatures ? featureCountOnCreate : undefined
);
if (!result.success) {
const errorMsg = result.error || 'Unknown error';
logger.error('[useSpecGeneration] Failed to start spec creation:', errorMsg);
setIsCreating(false);
setCurrentPhase('error');
setErrorMessage(errorMsg);
const errorLog = `[Error] Failed to start spec creation: ${errorMsg}\n`;
logsRef.current = errorLog;
setLogs(errorLog);
createSpecMutation.mutate(
{
projectOverview: projectOverview.trim(),
generateFeatures,
analyzeProject: analyzeProjectOnCreate,
featureCount: generateFeatures ? featureCountOnCreate : undefined,
},
{
onError: (error) => {
const errorMsg = error.message;
logger.error('[useSpecGeneration] Failed to create spec:', errorMsg);
setIsCreating(false);
setCurrentPhase('error');
setErrorMessage(errorMsg);
const errorLog = `[Error] Failed to create spec: ${errorMsg}\n`;
logsRef.current = errorLog;
setLogs(errorLog);
},
}
} catch (error) {
const errorMsg = error instanceof Error ? error.message : String(error);
logger.error('[useSpecGeneration] Failed to create spec:', errorMsg);
setIsCreating(false);
setCurrentPhase('error');
setErrorMessage(errorMsg);
const errorLog = `[Error] Failed to create spec: ${errorMsg}\n`;
logsRef.current = errorLog;
setLogs(errorLog);
}
);
}, [
currentProject,
projectOverview,
generateFeatures,
analyzeProjectOnCreate,
featureCountOnCreate,
createSpecMutation,
]);
const handleRegenerate = useCallback(async () => {
@@ -460,47 +453,34 @@ export function useSpecGeneration({ loadSpec }: UseSpecGenerationOptions) {
'[useSpecGeneration] Starting spec regeneration, generateFeatures:',
generateFeaturesOnRegenerate
);
try {
const api = getElectronAPI();
if (!api.specRegeneration) {
logger.error('[useSpecGeneration] Spec regeneration not available');
setIsRegenerating(false);
return;
}
const result = await api.specRegeneration.generate(
currentProject.path,
projectDefinition.trim(),
generateFeaturesOnRegenerate,
analyzeProjectOnRegenerate,
generateFeaturesOnRegenerate ? featureCountOnRegenerate : undefined
);
if (!result.success) {
const errorMsg = result.error || 'Unknown error';
logger.error('[useSpecGeneration] Failed to start regeneration:', errorMsg);
setIsRegenerating(false);
setCurrentPhase('error');
setErrorMessage(errorMsg);
const errorLog = `[Error] Failed to start regeneration: ${errorMsg}\n`;
logsRef.current = errorLog;
setLogs(errorLog);
regenerateSpecMutation.mutate(
{
projectDefinition: projectDefinition.trim(),
generateFeatures: generateFeaturesOnRegenerate,
analyzeProject: analyzeProjectOnRegenerate,
featureCount: generateFeaturesOnRegenerate ? featureCountOnRegenerate : undefined,
},
{
onError: (error) => {
const errorMsg = error.message;
logger.error('[useSpecGeneration] Failed to regenerate spec:', errorMsg);
setIsRegenerating(false);
setCurrentPhase('error');
setErrorMessage(errorMsg);
const errorLog = `[Error] Failed to regenerate spec: ${errorMsg}\n`;
logsRef.current = errorLog;
setLogs(errorLog);
},
}
} catch (error) {
const errorMsg = error instanceof Error ? error.message : String(error);
logger.error('[useSpecGeneration] Failed to regenerate spec:', errorMsg);
setIsRegenerating(false);
setCurrentPhase('error');
setErrorMessage(errorMsg);
const errorLog = `[Error] Failed to regenerate spec: ${errorMsg}\n`;
logsRef.current = errorLog;
setLogs(errorLog);
}
);
}, [
currentProject,
projectDefinition,
generateFeaturesOnRegenerate,
analyzeProjectOnRegenerate,
featureCountOnRegenerate,
regenerateSpecMutation,
]);
const handleGenerateFeatures = useCallback(async () => {
@@ -513,36 +493,20 @@ export function useSpecGeneration({ loadSpec }: UseSpecGenerationOptions) {
logsRef.current = '';
setLogs('');
logger.debug('[useSpecGeneration] Starting feature generation from existing spec');
try {
const api = getElectronAPI();
if (!api.specRegeneration) {
logger.error('[useSpecGeneration] Spec regeneration not available');
setIsGeneratingFeatures(false);
return;
}
const result = await api.specRegeneration.generateFeatures(currentProject.path);
if (!result.success) {
const errorMsg = result.error || 'Unknown error';
logger.error('[useSpecGeneration] Failed to start feature generation:', errorMsg);
generateFeaturesMutation.mutate(undefined, {
onError: (error) => {
const errorMsg = error.message;
logger.error('[useSpecGeneration] Failed to generate features:', errorMsg);
setIsGeneratingFeatures(false);
setCurrentPhase('error');
setErrorMessage(errorMsg);
const errorLog = `[Error] Failed to start feature generation: ${errorMsg}\n`;
const errorLog = `[Error] Failed to generate features: ${errorMsg}\n`;
logsRef.current = errorLog;
setLogs(errorLog);
}
} catch (error) {
const errorMsg = error instanceof Error ? error.message : String(error);
logger.error('[useSpecGeneration] Failed to generate features:', errorMsg);
setIsGeneratingFeatures(false);
setCurrentPhase('error');
setErrorMessage(errorMsg);
const errorLog = `[Error] Failed to generate features: ${errorMsg}\n`;
logsRef.current = errorLog;
setLogs(errorLog);
}
}, [currentProject]);
},
});
}, [currentProject, generateFeaturesMutation]);
return {
// Dialog state

View File

@@ -1,61 +1,53 @@
import { useEffect, useState, useCallback } from 'react';
import { createLogger } from '@automaker/utils/logger';
import { useAppStore } from '@/store/app-store';
const logger = createLogger('SpecLoading');
import { getElectronAPI } from '@/lib/electron';
import { useSpecFile, useSpecRegenerationStatus } from '@/hooks/queries';
import { useQueryClient } from '@tanstack/react-query';
import { queryKeys } from '@/lib/query-keys';
export function useSpecLoading() {
const { currentProject, setAppSpec } = useAppStore();
const [isLoading, setIsLoading] = useState(true);
const queryClient = useQueryClient();
const [specExists, setSpecExists] = useState(true);
const [isGenerationRunning, setIsGenerationRunning] = useState(false);
const loadSpec = useCallback(async () => {
if (!currentProject) return;
// React Query hooks
const specFileQuery = useSpecFile(currentProject?.path);
const statusQuery = useSpecRegenerationStatus(currentProject?.path);
setIsLoading(true);
try {
const api = getElectronAPI();
// Check if spec generation is running before trying to load
// This prevents showing "No App Specification Found" during generation
if (api.specRegeneration) {
const status = await api.specRegeneration.status(currentProject.path);
if (status.success && status.isRunning) {
logger.debug('Spec generation is running for this project, skipping load');
setIsGenerationRunning(true);
setIsLoading(false);
return;
}
}
// Always reset when generation is not running (handles edge case where api.specRegeneration might not be available)
setIsGenerationRunning(false);
const result = await api.readFile(`${currentProject.path}/.automaker/app_spec.txt`);
if (result.success && result.content) {
setAppSpec(result.content);
setSpecExists(true);
} else {
// File doesn't exist
setAppSpec('');
setSpecExists(false);
}
} catch (error) {
logger.error('Failed to load spec:', error);
setSpecExists(false);
} finally {
setIsLoading(false);
}
}, [currentProject, setAppSpec]);
const isGenerationRunning = statusQuery.data?.isRunning ?? false;
// Update app store and specExists when spec file data changes
useEffect(() => {
loadSpec();
}, [loadSpec]);
if (specFileQuery.data && !isGenerationRunning) {
setAppSpec(specFileQuery.data.content);
setSpecExists(specFileQuery.data.exists);
}
}, [specFileQuery.data, setAppSpec, isGenerationRunning]);
// Manual reload function (invalidates cache)
const loadSpec = useCallback(async () => {
if (!currentProject?.path) return;
// First check if generation is running
await queryClient.invalidateQueries({
queryKey: queryKeys.specRegeneration.status(currentProject.path),
});
const statusData = queryClient.getQueryData<{ isRunning: boolean }>(
queryKeys.specRegeneration.status(currentProject.path)
);
if (statusData?.isRunning) {
return;
}
// Invalidate and refetch spec file
await queryClient.invalidateQueries({
queryKey: queryKeys.spec.file(currentProject.path),
});
}, [currentProject?.path, queryClient]);
return {
isLoading,
isLoading: specFileQuery.isLoading,
specExists,
setSpecExists,
isGenerationRunning,

View File

@@ -1,28 +1,20 @@
import { useState } from 'react';
import { createLogger } from '@automaker/utils/logger';
import { useAppStore } from '@/store/app-store';
const logger = createLogger('SpecSave');
import { getElectronAPI } from '@/lib/electron';
import { useSaveSpec } from '@/hooks/mutations';
export function useSpecSave() {
const { currentProject, appSpec, setAppSpec } = useAppStore();
const [isSaving, setIsSaving] = useState(false);
const [hasChanges, setHasChanges] = useState(false);
// React Query mutation
const saveMutation = useSaveSpec(currentProject?.path ?? '');
const saveSpec = async () => {
if (!currentProject) return;
setIsSaving(true);
try {
const api = getElectronAPI();
await api.writeFile(`${currentProject.path}/.automaker/app_spec.txt`, appSpec);
setHasChanges(false);
} catch (error) {
logger.error('Failed to save spec:', error);
} finally {
setIsSaving(false);
}
saveMutation.mutate(appSpec, {
onSuccess: () => setHasChanges(false),
});
};
const handleChange = (value: string) => {
@@ -31,7 +23,7 @@ export function useSpecSave() {
};
return {
isSaving,
isSaving: saveMutation.isPending,
hasChanges,
setHasChanges,
saveSpec,