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

@@ -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,