mirror of
https://github.com/AutoMaker-Org/automaker.git
synced 2026-02-04 09:13:08 +00:00
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:
@@ -1,4 +1,3 @@
|
|||||||
import { useState, useEffect, useCallback } from 'react';
|
|
||||||
import {
|
import {
|
||||||
Dialog,
|
Dialog,
|
||||||
DialogContent,
|
DialogContent,
|
||||||
@@ -9,7 +8,7 @@ import {
|
|||||||
} from '@/components/ui/dialog';
|
} from '@/components/ui/dialog';
|
||||||
import { Button } from '@/components/ui/button';
|
import { Button } from '@/components/ui/button';
|
||||||
import { Folder, Loader2, FolderOpen, AlertCircle } from 'lucide-react';
|
import { Folder, Loader2, FolderOpen, AlertCircle } from 'lucide-react';
|
||||||
import { getHttpApiClient } from '@/lib/http-api-client';
|
import { useWorkspaceDirectories } from '@/hooks/queries';
|
||||||
|
|
||||||
interface WorkspaceDirectory {
|
interface WorkspaceDirectory {
|
||||||
name: string;
|
name: string;
|
||||||
@@ -23,41 +22,15 @@ interface WorkspacePickerModalProps {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function WorkspacePickerModal({ open, onOpenChange, onSelect }: WorkspacePickerModalProps) {
|
export function WorkspacePickerModal({ open, onOpenChange, onSelect }: WorkspacePickerModalProps) {
|
||||||
const [isLoading, setIsLoading] = useState(false);
|
// React Query hook - only fetch when modal is open
|
||||||
const [directories, setDirectories] = useState<WorkspaceDirectory[]>([]);
|
const { data: directories = [], isLoading, error, refetch } = useWorkspaceDirectories(open);
|
||||||
const [error, setError] = useState<string | null>(null);
|
|
||||||
|
|
||||||
const loadDirectories = useCallback(async () => {
|
|
||||||
setIsLoading(true);
|
|
||||||
setError(null);
|
|
||||||
|
|
||||||
try {
|
|
||||||
const client = getHttpApiClient();
|
|
||||||
const result = await client.workspace.getDirectories();
|
|
||||||
|
|
||||||
if (result.success && result.directories) {
|
|
||||||
setDirectories(result.directories);
|
|
||||||
} else {
|
|
||||||
setError(result.error || 'Failed to load directories');
|
|
||||||
}
|
|
||||||
} catch (err) {
|
|
||||||
setError(err instanceof Error ? err.message : 'Failed to load directories');
|
|
||||||
} finally {
|
|
||||||
setIsLoading(false);
|
|
||||||
}
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
// Load directories when modal opens
|
|
||||||
useEffect(() => {
|
|
||||||
if (open) {
|
|
||||||
loadDirectories();
|
|
||||||
}
|
|
||||||
}, [open, loadDirectories]);
|
|
||||||
|
|
||||||
const handleSelect = (dir: WorkspaceDirectory) => {
|
const handleSelect = (dir: WorkspaceDirectory) => {
|
||||||
onSelect(dir.path, dir.name);
|
onSelect(dir.path, dir.name);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const errorMessage = error instanceof Error ? error.message : null;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Dialog open={open} onOpenChange={onOpenChange}>
|
<Dialog open={open} onOpenChange={onOpenChange}>
|
||||||
<DialogContent className="bg-card border-border max-w-lg max-h-[80vh] flex flex-col">
|
<DialogContent className="bg-card border-border max-w-lg max-h-[80vh] flex flex-col">
|
||||||
@@ -79,19 +52,19 @@ export function WorkspacePickerModal({ open, onOpenChange, onSelect }: Workspace
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{error && !isLoading && (
|
{errorMessage && !isLoading && (
|
||||||
<div className="flex flex-col items-center justify-center h-full gap-3 text-center px-4">
|
<div className="flex flex-col items-center justify-center h-full gap-3 text-center px-4">
|
||||||
<div className="w-12 h-12 rounded-full bg-destructive/10 flex items-center justify-center">
|
<div className="w-12 h-12 rounded-full bg-destructive/10 flex items-center justify-center">
|
||||||
<AlertCircle className="w-6 h-6 text-destructive" />
|
<AlertCircle className="w-6 h-6 text-destructive" />
|
||||||
</div>
|
</div>
|
||||||
<p className="text-sm text-destructive">{error}</p>
|
<p className="text-sm text-destructive">{errorMessage}</p>
|
||||||
<Button variant="secondary" size="sm" onClick={loadDirectories} className="mt-2">
|
<Button variant="secondary" size="sm" onClick={() => refetch()} className="mt-2">
|
||||||
Try Again
|
Try Again
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{!isLoading && !error && directories.length === 0 && (
|
{!isLoading && !errorMessage && directories.length === 0 && (
|
||||||
<div className="flex flex-col items-center justify-center h-full gap-3 text-center px-4">
|
<div className="flex flex-col items-center justify-center h-full gap-3 text-center px-4">
|
||||||
<div className="w-12 h-12 rounded-full bg-muted flex items-center justify-center">
|
<div className="w-12 h-12 rounded-full bg-muted flex items-center justify-center">
|
||||||
<Folder className="w-6 h-6 text-muted-foreground" />
|
<Folder className="w-6 h-6 text-muted-foreground" />
|
||||||
@@ -102,7 +75,7 @@ export function WorkspacePickerModal({ open, onOpenChange, onSelect }: Workspace
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{!isLoading && !error && directories.length > 0 && (
|
{!isLoading && !errorMessage && directories.length > 0 && (
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
{directories.map((dir) => (
|
{directories.map((dir) => (
|
||||||
<button
|
<button
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { useState, useEffect } from 'react';
|
import { useState, useEffect, useCallback } from 'react';
|
||||||
import { createLogger } from '@automaker/utils/logger';
|
import { createLogger } from '@automaker/utils/logger';
|
||||||
|
import { useQueryClient } from '@tanstack/react-query';
|
||||||
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
|
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
|
||||||
|
|
||||||
const logger = createLogger('SessionManager');
|
const logger = createLogger('SessionManager');
|
||||||
@@ -22,6 +23,8 @@ import { cn } from '@/lib/utils';
|
|||||||
import type { SessionListItem } from '@/types/electron';
|
import type { SessionListItem } from '@/types/electron';
|
||||||
import { useKeyboardShortcutsConfig } from '@/hooks/use-keyboard-shortcuts';
|
import { useKeyboardShortcutsConfig } from '@/hooks/use-keyboard-shortcuts';
|
||||||
import { getElectronAPI } from '@/lib/electron';
|
import { getElectronAPI } from '@/lib/electron';
|
||||||
|
import { useSessions } from '@/hooks/queries';
|
||||||
|
import { queryKeys } from '@/lib/query-keys';
|
||||||
import { DeleteSessionDialog } from '@/components/dialogs/delete-session-dialog';
|
import { DeleteSessionDialog } from '@/components/dialogs/delete-session-dialog';
|
||||||
import { DeleteAllArchivedSessionsDialog } from '@/components/dialogs/delete-all-archived-sessions-dialog';
|
import { DeleteAllArchivedSessionsDialog } from '@/components/dialogs/delete-all-archived-sessions-dialog';
|
||||||
|
|
||||||
@@ -102,7 +105,7 @@ export function SessionManager({
|
|||||||
onQuickCreateRef,
|
onQuickCreateRef,
|
||||||
}: SessionManagerProps) {
|
}: SessionManagerProps) {
|
||||||
const shortcuts = useKeyboardShortcutsConfig();
|
const shortcuts = useKeyboardShortcutsConfig();
|
||||||
const [sessions, setSessions] = useState<SessionListItem[]>([]);
|
const queryClient = useQueryClient();
|
||||||
const [activeTab, setActiveTab] = useState<'active' | 'archived'>('active');
|
const [activeTab, setActiveTab] = useState<'active' | 'archived'>('active');
|
||||||
const [editingSessionId, setEditingSessionId] = useState<string | null>(null);
|
const [editingSessionId, setEditingSessionId] = useState<string | null>(null);
|
||||||
const [editingName, setEditingName] = useState('');
|
const [editingName, setEditingName] = useState('');
|
||||||
@@ -113,8 +116,11 @@ export function SessionManager({
|
|||||||
const [sessionToDelete, setSessionToDelete] = useState<SessionListItem | null>(null);
|
const [sessionToDelete, setSessionToDelete] = useState<SessionListItem | null>(null);
|
||||||
const [isDeleteAllArchivedDialogOpen, setIsDeleteAllArchivedDialogOpen] = useState(false);
|
const [isDeleteAllArchivedDialogOpen, setIsDeleteAllArchivedDialogOpen] = useState(false);
|
||||||
|
|
||||||
|
// Use React Query for sessions list - always include archived, filter client-side
|
||||||
|
const { data: sessions = [], refetch: refetchSessions } = useSessions(true);
|
||||||
|
|
||||||
// Check running state for all sessions
|
// Check running state for all sessions
|
||||||
const checkRunningSessions = async (sessionList: SessionListItem[]) => {
|
const checkRunningSessions = useCallback(async (sessionList: SessionListItem[]) => {
|
||||||
const api = getElectronAPI();
|
const api = getElectronAPI();
|
||||||
if (!api?.agent) return;
|
if (!api?.agent) return;
|
||||||
|
|
||||||
@@ -134,26 +140,25 @@ export function SessionManager({
|
|||||||
}
|
}
|
||||||
|
|
||||||
setRunningSessions(runningIds);
|
setRunningSessions(runningIds);
|
||||||
};
|
|
||||||
|
|
||||||
// Load sessions
|
|
||||||
const loadSessions = async () => {
|
|
||||||
const api = getElectronAPI();
|
|
||||||
if (!api?.sessions) return;
|
|
||||||
|
|
||||||
// Always load all sessions and filter client-side
|
|
||||||
const result = await api.sessions.list(true);
|
|
||||||
if (result.success && result.sessions) {
|
|
||||||
setSessions(result.sessions);
|
|
||||||
// Check running state for all sessions
|
|
||||||
await checkRunningSessions(result.sessions);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
loadSessions();
|
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
// Helper to invalidate sessions cache and refetch
|
||||||
|
const invalidateSessions = useCallback(async () => {
|
||||||
|
await queryClient.invalidateQueries({ queryKey: queryKeys.sessions.all(true) });
|
||||||
|
// Also check running state after invalidation
|
||||||
|
const result = await refetchSessions();
|
||||||
|
if (result.data) {
|
||||||
|
await checkRunningSessions(result.data);
|
||||||
|
}
|
||||||
|
}, [queryClient, refetchSessions, checkRunningSessions]);
|
||||||
|
|
||||||
|
// Check running state on initial load
|
||||||
|
useEffect(() => {
|
||||||
|
if (sessions.length > 0) {
|
||||||
|
checkRunningSessions(sessions);
|
||||||
|
}
|
||||||
|
}, [sessions.length > 0]); // Only run when sessions first load
|
||||||
|
|
||||||
// Periodically check running state for sessions (useful for detecting when agents finish)
|
// Periodically check running state for sessions (useful for detecting when agents finish)
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
// Only poll if there are running sessions
|
// Only poll if there are running sessions
|
||||||
@@ -166,7 +171,7 @@ export function SessionManager({
|
|||||||
}, 3000); // Check every 3 seconds
|
}, 3000); // Check every 3 seconds
|
||||||
|
|
||||||
return () => clearInterval(interval);
|
return () => clearInterval(interval);
|
||||||
}, [sessions, runningSessions.size, isCurrentSessionThinking]);
|
}, [sessions, runningSessions.size, isCurrentSessionThinking, checkRunningSessions]);
|
||||||
|
|
||||||
// Create new session with random name
|
// Create new session with random name
|
||||||
const handleCreateSession = async () => {
|
const handleCreateSession = async () => {
|
||||||
@@ -180,7 +185,7 @@ export function SessionManager({
|
|||||||
if (result.success && result.session?.id) {
|
if (result.success && result.session?.id) {
|
||||||
setNewSessionName('');
|
setNewSessionName('');
|
||||||
setIsCreating(false);
|
setIsCreating(false);
|
||||||
await loadSessions();
|
await invalidateSessions();
|
||||||
onSelectSession(result.session.id);
|
onSelectSession(result.session.id);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -195,7 +200,7 @@ export function SessionManager({
|
|||||||
const result = await api.sessions.create(sessionName, projectPath, projectPath);
|
const result = await api.sessions.create(sessionName, projectPath, projectPath);
|
||||||
|
|
||||||
if (result.success && result.session?.id) {
|
if (result.success && result.session?.id) {
|
||||||
await loadSessions();
|
await invalidateSessions();
|
||||||
onSelectSession(result.session.id);
|
onSelectSession(result.session.id);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -222,7 +227,7 @@ export function SessionManager({
|
|||||||
if (result.success) {
|
if (result.success) {
|
||||||
setEditingSessionId(null);
|
setEditingSessionId(null);
|
||||||
setEditingName('');
|
setEditingName('');
|
||||||
await loadSessions();
|
await invalidateSessions();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -241,7 +246,7 @@ export function SessionManager({
|
|||||||
if (currentSessionId === sessionId) {
|
if (currentSessionId === sessionId) {
|
||||||
onSelectSession(null);
|
onSelectSession(null);
|
||||||
}
|
}
|
||||||
await loadSessions();
|
await invalidateSessions();
|
||||||
} else {
|
} else {
|
||||||
logger.error('[SessionManager] Archive failed:', result.error);
|
logger.error('[SessionManager] Archive failed:', result.error);
|
||||||
}
|
}
|
||||||
@@ -261,7 +266,7 @@ export function SessionManager({
|
|||||||
try {
|
try {
|
||||||
const result = await api.sessions.unarchive(sessionId);
|
const result = await api.sessions.unarchive(sessionId);
|
||||||
if (result.success) {
|
if (result.success) {
|
||||||
await loadSessions();
|
await invalidateSessions();
|
||||||
} else {
|
} else {
|
||||||
logger.error('[SessionManager] Unarchive failed:', result.error);
|
logger.error('[SessionManager] Unarchive failed:', result.error);
|
||||||
}
|
}
|
||||||
@@ -283,7 +288,7 @@ export function SessionManager({
|
|||||||
|
|
||||||
const result = await api.sessions.delete(sessionId);
|
const result = await api.sessions.delete(sessionId);
|
||||||
if (result.success) {
|
if (result.success) {
|
||||||
await loadSessions();
|
await invalidateSessions();
|
||||||
if (currentSessionId === sessionId) {
|
if (currentSessionId === sessionId) {
|
||||||
// Switch to another session or create a new one
|
// Switch to another session or create a new one
|
||||||
const activeSessionsList = sessions.filter((s) => !s.isArchived);
|
const activeSessionsList = sessions.filter((s) => !s.isArchived);
|
||||||
@@ -305,7 +310,7 @@ export function SessionManager({
|
|||||||
await api.sessions.delete(session.id);
|
await api.sessions.delete(session.id);
|
||||||
}
|
}
|
||||||
|
|
||||||
await loadSessions();
|
await invalidateSessions();
|
||||||
setIsDeleteAllArchivedDialogOpen(false);
|
setIsDeleteAllArchivedDialogOpen(false);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
import { useState, useEffect, useMemo, useCallback } from 'react';
|
import { useState, useMemo } from 'react';
|
||||||
import { getElectronAPI } from '@/lib/electron';
|
|
||||||
import { cn } from '@/lib/utils';
|
import { cn } from '@/lib/utils';
|
||||||
import {
|
import {
|
||||||
File,
|
File,
|
||||||
@@ -15,6 +14,7 @@ import {
|
|||||||
AlertCircle,
|
AlertCircle,
|
||||||
} from 'lucide-react';
|
} from 'lucide-react';
|
||||||
import { Button } from './button';
|
import { Button } from './button';
|
||||||
|
import { useWorktreeDiffs, useGitDiffs } from '@/hooks/queries';
|
||||||
import type { FileStatus } from '@/types/electron';
|
import type { FileStatus } from '@/types/electron';
|
||||||
|
|
||||||
interface GitDiffPanelProps {
|
interface GitDiffPanelProps {
|
||||||
@@ -350,56 +350,44 @@ export function GitDiffPanel({
|
|||||||
useWorktrees = false,
|
useWorktrees = false,
|
||||||
}: GitDiffPanelProps) {
|
}: GitDiffPanelProps) {
|
||||||
const [isExpanded, setIsExpanded] = useState(!compact);
|
const [isExpanded, setIsExpanded] = useState(!compact);
|
||||||
const [isLoading, setIsLoading] = useState(false);
|
|
||||||
const [error, setError] = useState<string | null>(null);
|
|
||||||
const [files, setFiles] = useState<FileStatus[]>([]);
|
|
||||||
const [diffContent, setDiffContent] = useState<string>('');
|
|
||||||
const [expandedFiles, setExpandedFiles] = useState<Set<string>>(new Set());
|
const [expandedFiles, setExpandedFiles] = useState<Set<string>>(new Set());
|
||||||
|
|
||||||
const loadDiffs = useCallback(async () => {
|
// Use worktree diffs hook when worktrees are enabled and panel is expanded
|
||||||
setIsLoading(true);
|
// Pass undefined for featureId when not using worktrees to disable the query
|
||||||
setError(null);
|
const {
|
||||||
try {
|
data: worktreeDiffsData,
|
||||||
const api = getElectronAPI();
|
isLoading: isLoadingWorktree,
|
||||||
|
error: worktreeError,
|
||||||
|
refetch: refetchWorktree,
|
||||||
|
} = useWorktreeDiffs(
|
||||||
|
useWorktrees && isExpanded ? projectPath : undefined,
|
||||||
|
useWorktrees && isExpanded ? featureId : undefined
|
||||||
|
);
|
||||||
|
|
||||||
// Use worktree API if worktrees are enabled, otherwise use git API for main project
|
// Use git diffs hook when worktrees are disabled and panel is expanded
|
||||||
if (useWorktrees) {
|
const {
|
||||||
if (!api?.worktree?.getDiffs) {
|
data: gitDiffsData,
|
||||||
throw new Error('Worktree API not available');
|
isLoading: isLoadingGit,
|
||||||
}
|
error: gitError,
|
||||||
const result = await api.worktree.getDiffs(projectPath, featureId);
|
refetch: refetchGit,
|
||||||
if (result.success) {
|
} = useGitDiffs(projectPath, !useWorktrees && isExpanded);
|
||||||
setFiles(result.files || []);
|
|
||||||
setDiffContent(result.diff || '');
|
|
||||||
} else {
|
|
||||||
setError(result.error || 'Failed to load diffs');
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// Use git API for main project diffs
|
|
||||||
if (!api?.git?.getDiffs) {
|
|
||||||
throw new Error('Git API not available');
|
|
||||||
}
|
|
||||||
const result = await api.git.getDiffs(projectPath);
|
|
||||||
if (result.success) {
|
|
||||||
setFiles(result.files || []);
|
|
||||||
setDiffContent(result.diff || '');
|
|
||||||
} else {
|
|
||||||
setError(result.error || 'Failed to load diffs');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (err) {
|
|
||||||
setError(err instanceof Error ? err.message : 'Failed to load diffs');
|
|
||||||
} finally {
|
|
||||||
setIsLoading(false);
|
|
||||||
}
|
|
||||||
}, [projectPath, featureId, useWorktrees]);
|
|
||||||
|
|
||||||
// Load diffs when expanded
|
// Select the appropriate data based on useWorktrees prop
|
||||||
useEffect(() => {
|
const diffsData = useWorktrees ? worktreeDiffsData : gitDiffsData;
|
||||||
if (isExpanded) {
|
const isLoading = useWorktrees ? isLoadingWorktree : isLoadingGit;
|
||||||
loadDiffs();
|
const queryError = useWorktrees ? worktreeError : gitError;
|
||||||
}
|
|
||||||
}, [isExpanded, loadDiffs]);
|
// Extract files and diff content from the data
|
||||||
|
const files: FileStatus[] = diffsData?.files ?? [];
|
||||||
|
const diffContent = diffsData?.diff ?? '';
|
||||||
|
const error = queryError
|
||||||
|
? queryError instanceof Error
|
||||||
|
? queryError.message
|
||||||
|
: 'Failed to load diffs'
|
||||||
|
: null;
|
||||||
|
|
||||||
|
// Refetch function
|
||||||
|
const loadDiffs = useWorktrees ? refetchWorktree : refetchGit;
|
||||||
|
|
||||||
const parsedDiffs = useMemo(() => parseDiff(diffContent), [diffContent]);
|
const parsedDiffs = useMemo(() => parseDiff(diffContent), [diffContent]);
|
||||||
|
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ import { Card, CardContent } from '@/components/ui/card';
|
|||||||
import { useGuidedPrompts } from '@/hooks/use-guided-prompts';
|
import { useGuidedPrompts } from '@/hooks/use-guided-prompts';
|
||||||
import { useIdeationStore } from '@/store/ideation-store';
|
import { useIdeationStore } from '@/store/ideation-store';
|
||||||
import { useAppStore } from '@/store/app-store';
|
import { useAppStore } from '@/store/app-store';
|
||||||
import { getElectronAPI } from '@/lib/electron';
|
import { useGenerateIdeationSuggestions } from '@/hooks/mutations';
|
||||||
import { toast } from 'sonner';
|
import { toast } from 'sonner';
|
||||||
import { useNavigate } from '@tanstack/react-router';
|
import { useNavigate } from '@tanstack/react-router';
|
||||||
import type { IdeaCategory, IdeationPrompt } from '@automaker/types';
|
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 [loadingPromptId, setLoadingPromptId] = useState<string | null>(null);
|
||||||
const [startedPrompts, setStartedPrompts] = useState<Set<string>>(new Set());
|
const [startedPrompts, setStartedPrompts] = useState<Set<string>>(new Set());
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
|
|
||||||
|
// React Query mutation
|
||||||
|
const generateMutation = useGenerateIdeationSuggestions(currentProject?.path ?? '');
|
||||||
const {
|
const {
|
||||||
getPromptsByCategory,
|
getPromptsByCategory,
|
||||||
isLoading: isLoadingPrompts,
|
isLoading: isLoadingPrompts,
|
||||||
@@ -56,7 +59,7 @@ export function PromptList({ category, onBack }: PromptListProps) {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (loadingPromptId || generatingPromptIds.has(prompt.id)) return;
|
if (loadingPromptId || generateMutation.isPending || generatingPromptIds.has(prompt.id)) return;
|
||||||
|
|
||||||
setLoadingPromptId(prompt.id);
|
setLoadingPromptId(prompt.id);
|
||||||
|
|
||||||
@@ -68,42 +71,31 @@ export function PromptList({ category, onBack }: PromptListProps) {
|
|||||||
toast.info(`Generating ideas for "${prompt.title}"...`);
|
toast.info(`Generating ideas for "${prompt.title}"...`);
|
||||||
setMode('dashboard');
|
setMode('dashboard');
|
||||||
|
|
||||||
try {
|
generateMutation.mutate(
|
||||||
const api = getElectronAPI();
|
{ promptId: prompt.id, category },
|
||||||
const result = await api.ideation?.generateSuggestions(
|
{
|
||||||
currentProject.path,
|
onSuccess: (data) => {
|
||||||
prompt.id,
|
updateJobStatus(jobId, 'ready', data.suggestions);
|
||||||
category
|
toast.success(`Generated ${data.suggestions.length} ideas for "${prompt.title}"`, {
|
||||||
);
|
duration: 10000,
|
||||||
|
action: {
|
||||||
if (result?.success && result.suggestions) {
|
label: 'View Ideas',
|
||||||
updateJobStatus(jobId, 'ready', result.suggestions);
|
onClick: () => {
|
||||||
toast.success(`Generated ${result.suggestions.length} ideas for "${prompt.title}"`, {
|
setMode('dashboard');
|
||||||
duration: 10000,
|
navigate({ to: '/ideation' });
|
||||||
action: {
|
},
|
||||||
label: 'View Ideas',
|
|
||||||
onClick: () => {
|
|
||||||
setMode('dashboard');
|
|
||||||
navigate({ to: '/ideation' });
|
|
||||||
},
|
},
|
||||||
},
|
});
|
||||||
});
|
setLoadingPromptId(null);
|
||||||
} else {
|
},
|
||||||
updateJobStatus(
|
onError: (error) => {
|
||||||
jobId,
|
console.error('Failed to generate suggestions:', error);
|
||||||
'error',
|
updateJobStatus(jobId, 'error', undefined, error.message);
|
||||||
undefined,
|
toast.error(error.message);
|
||||||
result?.error || 'Failed to generate suggestions'
|
setLoadingPromptId(null);
|
||||||
);
|
},
|
||||||
toast.error(result?.error || 'Failed to generate suggestions');
|
|
||||||
}
|
}
|
||||||
} 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 (
|
return (
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import { createElement } from 'react';
|
|||||||
import { SPEC_FILE_WRITE_DELAY, STATUS_CHECK_INTERVAL_MS } from '../constants';
|
import { SPEC_FILE_WRITE_DELAY, STATUS_CHECK_INTERVAL_MS } from '../constants';
|
||||||
import type { FeatureCount } from '../types';
|
import type { FeatureCount } from '../types';
|
||||||
import type { SpecRegenerationEvent } from '@/types/electron';
|
import type { SpecRegenerationEvent } from '@/types/electron';
|
||||||
|
import { useCreateSpec, useRegenerateSpec, useGenerateFeatures } from '@/hooks/mutations';
|
||||||
|
|
||||||
interface UseSpecGenerationOptions {
|
interface UseSpecGenerationOptions {
|
||||||
loadSpec: () => Promise<void>;
|
loadSpec: () => Promise<void>;
|
||||||
@@ -18,6 +19,11 @@ interface UseSpecGenerationOptions {
|
|||||||
export function useSpecGeneration({ loadSpec }: UseSpecGenerationOptions) {
|
export function useSpecGeneration({ loadSpec }: UseSpecGenerationOptions) {
|
||||||
const { currentProject } = useAppStore();
|
const { currentProject } = useAppStore();
|
||||||
|
|
||||||
|
// React Query mutations
|
||||||
|
const createSpecMutation = useCreateSpec(currentProject?.path ?? '');
|
||||||
|
const regenerateSpecMutation = useRegenerateSpec(currentProject?.path ?? '');
|
||||||
|
const generateFeaturesMutation = useGenerateFeatures(currentProject?.path ?? '');
|
||||||
|
|
||||||
// Dialog visibility state
|
// Dialog visibility state
|
||||||
const [showCreateDialog, setShowCreateDialog] = useState(false);
|
const [showCreateDialog, setShowCreateDialog] = useState(false);
|
||||||
const [showRegenerateDialog, setShowRegenerateDialog] = useState(false);
|
const [showRegenerateDialog, setShowRegenerateDialog] = useState(false);
|
||||||
@@ -404,47 +410,34 @@ export function useSpecGeneration({ loadSpec }: UseSpecGenerationOptions) {
|
|||||||
logsRef.current = '';
|
logsRef.current = '';
|
||||||
setLogs('');
|
setLogs('');
|
||||||
logger.debug('[useSpecGeneration] Starting spec creation, generateFeatures:', generateFeatures);
|
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) {
|
createSpecMutation.mutate(
|
||||||
const errorMsg = result.error || 'Unknown error';
|
{
|
||||||
logger.error('[useSpecGeneration] Failed to start spec creation:', errorMsg);
|
projectOverview: projectOverview.trim(),
|
||||||
setIsCreating(false);
|
generateFeatures,
|
||||||
setCurrentPhase('error');
|
analyzeProject: analyzeProjectOnCreate,
|
||||||
setErrorMessage(errorMsg);
|
featureCount: generateFeatures ? featureCountOnCreate : undefined,
|
||||||
const errorLog = `[Error] Failed to start spec creation: ${errorMsg}\n`;
|
},
|
||||||
logsRef.current = errorLog;
|
{
|
||||||
setLogs(errorLog);
|
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,
|
currentProject,
|
||||||
projectOverview,
|
projectOverview,
|
||||||
generateFeatures,
|
generateFeatures,
|
||||||
analyzeProjectOnCreate,
|
analyzeProjectOnCreate,
|
||||||
featureCountOnCreate,
|
featureCountOnCreate,
|
||||||
|
createSpecMutation,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
const handleRegenerate = useCallback(async () => {
|
const handleRegenerate = useCallback(async () => {
|
||||||
@@ -460,47 +453,34 @@ export function useSpecGeneration({ loadSpec }: UseSpecGenerationOptions) {
|
|||||||
'[useSpecGeneration] Starting spec regeneration, generateFeatures:',
|
'[useSpecGeneration] Starting spec regeneration, generateFeatures:',
|
||||||
generateFeaturesOnRegenerate
|
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) {
|
regenerateSpecMutation.mutate(
|
||||||
const errorMsg = result.error || 'Unknown error';
|
{
|
||||||
logger.error('[useSpecGeneration] Failed to start regeneration:', errorMsg);
|
projectDefinition: projectDefinition.trim(),
|
||||||
setIsRegenerating(false);
|
generateFeatures: generateFeaturesOnRegenerate,
|
||||||
setCurrentPhase('error');
|
analyzeProject: analyzeProjectOnRegenerate,
|
||||||
setErrorMessage(errorMsg);
|
featureCount: generateFeaturesOnRegenerate ? featureCountOnRegenerate : undefined,
|
||||||
const errorLog = `[Error] Failed to start regeneration: ${errorMsg}\n`;
|
},
|
||||||
logsRef.current = errorLog;
|
{
|
||||||
setLogs(errorLog);
|
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,
|
currentProject,
|
||||||
projectDefinition,
|
projectDefinition,
|
||||||
generateFeaturesOnRegenerate,
|
generateFeaturesOnRegenerate,
|
||||||
analyzeProjectOnRegenerate,
|
analyzeProjectOnRegenerate,
|
||||||
featureCountOnRegenerate,
|
featureCountOnRegenerate,
|
||||||
|
regenerateSpecMutation,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
const handleGenerateFeatures = useCallback(async () => {
|
const handleGenerateFeatures = useCallback(async () => {
|
||||||
@@ -513,36 +493,20 @@ export function useSpecGeneration({ loadSpec }: UseSpecGenerationOptions) {
|
|||||||
logsRef.current = '';
|
logsRef.current = '';
|
||||||
setLogs('');
|
setLogs('');
|
||||||
logger.debug('[useSpecGeneration] Starting feature generation from existing spec');
|
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) {
|
generateFeaturesMutation.mutate(undefined, {
|
||||||
const errorMsg = result.error || 'Unknown error';
|
onError: (error) => {
|
||||||
logger.error('[useSpecGeneration] Failed to start feature generation:', errorMsg);
|
const errorMsg = error.message;
|
||||||
|
logger.error('[useSpecGeneration] Failed to generate features:', errorMsg);
|
||||||
setIsGeneratingFeatures(false);
|
setIsGeneratingFeatures(false);
|
||||||
setCurrentPhase('error');
|
setCurrentPhase('error');
|
||||||
setErrorMessage(errorMsg);
|
setErrorMessage(errorMsg);
|
||||||
const errorLog = `[Error] Failed to start feature generation: ${errorMsg}\n`;
|
const errorLog = `[Error] Failed to generate features: ${errorMsg}\n`;
|
||||||
logsRef.current = errorLog;
|
logsRef.current = errorLog;
|
||||||
setLogs(errorLog);
|
setLogs(errorLog);
|
||||||
}
|
},
|
||||||
} catch (error) {
|
});
|
||||||
const errorMsg = error instanceof Error ? error.message : String(error);
|
}, [currentProject, generateFeaturesMutation]);
|
||||||
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]);
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
// Dialog state
|
// Dialog state
|
||||||
|
|||||||
@@ -1,61 +1,53 @@
|
|||||||
import { useEffect, useState, useCallback } from 'react';
|
import { useEffect, useState, useCallback } from 'react';
|
||||||
import { createLogger } from '@automaker/utils/logger';
|
|
||||||
import { useAppStore } from '@/store/app-store';
|
import { useAppStore } from '@/store/app-store';
|
||||||
|
import { useSpecFile, useSpecRegenerationStatus } from '@/hooks/queries';
|
||||||
const logger = createLogger('SpecLoading');
|
import { useQueryClient } from '@tanstack/react-query';
|
||||||
import { getElectronAPI } from '@/lib/electron';
|
import { queryKeys } from '@/lib/query-keys';
|
||||||
|
|
||||||
export function useSpecLoading() {
|
export function useSpecLoading() {
|
||||||
const { currentProject, setAppSpec } = useAppStore();
|
const { currentProject, setAppSpec } = useAppStore();
|
||||||
const [isLoading, setIsLoading] = useState(true);
|
const queryClient = useQueryClient();
|
||||||
const [specExists, setSpecExists] = useState(true);
|
const [specExists, setSpecExists] = useState(true);
|
||||||
const [isGenerationRunning, setIsGenerationRunning] = useState(false);
|
|
||||||
|
|
||||||
const loadSpec = useCallback(async () => {
|
// React Query hooks
|
||||||
if (!currentProject) return;
|
const specFileQuery = useSpecFile(currentProject?.path);
|
||||||
|
const statusQuery = useSpecRegenerationStatus(currentProject?.path);
|
||||||
|
|
||||||
setIsLoading(true);
|
const isGenerationRunning = statusQuery.data?.isRunning ?? false;
|
||||||
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]);
|
|
||||||
|
|
||||||
|
// Update app store and specExists when spec file data changes
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
loadSpec();
|
if (specFileQuery.data && !isGenerationRunning) {
|
||||||
}, [loadSpec]);
|
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 {
|
return {
|
||||||
isLoading,
|
isLoading: specFileQuery.isLoading,
|
||||||
specExists,
|
specExists,
|
||||||
setSpecExists,
|
setSpecExists,
|
||||||
isGenerationRunning,
|
isGenerationRunning,
|
||||||
|
|||||||
@@ -1,28 +1,20 @@
|
|||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
import { createLogger } from '@automaker/utils/logger';
|
|
||||||
import { useAppStore } from '@/store/app-store';
|
import { useAppStore } from '@/store/app-store';
|
||||||
|
import { useSaveSpec } from '@/hooks/mutations';
|
||||||
const logger = createLogger('SpecSave');
|
|
||||||
import { getElectronAPI } from '@/lib/electron';
|
|
||||||
|
|
||||||
export function useSpecSave() {
|
export function useSpecSave() {
|
||||||
const { currentProject, appSpec, setAppSpec } = useAppStore();
|
const { currentProject, appSpec, setAppSpec } = useAppStore();
|
||||||
const [isSaving, setIsSaving] = useState(false);
|
|
||||||
const [hasChanges, setHasChanges] = useState(false);
|
const [hasChanges, setHasChanges] = useState(false);
|
||||||
|
|
||||||
|
// React Query mutation
|
||||||
|
const saveMutation = useSaveSpec(currentProject?.path ?? '');
|
||||||
|
|
||||||
const saveSpec = async () => {
|
const saveSpec = async () => {
|
||||||
if (!currentProject) return;
|
if (!currentProject) return;
|
||||||
|
|
||||||
setIsSaving(true);
|
saveMutation.mutate(appSpec, {
|
||||||
try {
|
onSuccess: () => setHasChanges(false),
|
||||||
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);
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleChange = (value: string) => {
|
const handleChange = (value: string) => {
|
||||||
@@ -31,7 +23,7 @@ export function useSpecSave() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
return {
|
return {
|
||||||
isSaving,
|
isSaving: saveMutation.isPending,
|
||||||
hasChanges,
|
hasChanges,
|
||||||
setHasChanges,
|
setHasChanges,
|
||||||
saveSpec,
|
saveSpec,
|
||||||
|
|||||||
@@ -1,36 +1,26 @@
|
|||||||
import { useCallback } from 'react';
|
import { useCallback } from 'react';
|
||||||
import { createLogger } from '@automaker/utils/logger';
|
|
||||||
import { useAppStore } from '@/store/app-store';
|
import { useAppStore } from '@/store/app-store';
|
||||||
import { getHttpApiClient } from '@/lib/http-api-client';
|
import { useUpdateProjectSettings } from '@/hooks/mutations';
|
||||||
import { toast } from 'sonner';
|
|
||||||
|
|
||||||
const logger = createLogger('BoardBackground');
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Hook for managing board background settings with automatic persistence to server
|
* Hook for managing board background settings with automatic persistence to server.
|
||||||
|
* Uses React Query mutation for server persistence with automatic error handling.
|
||||||
*/
|
*/
|
||||||
export function useBoardBackgroundSettings() {
|
export function useBoardBackgroundSettings() {
|
||||||
const store = useAppStore();
|
const store = useAppStore();
|
||||||
const httpClient = getHttpApiClient();
|
|
||||||
|
// Get the mutation without a fixed project path - we'll pass it with each call
|
||||||
|
const updateProjectSettings = useUpdateProjectSettings();
|
||||||
|
|
||||||
// Helper to persist settings to server
|
// Helper to persist settings to server
|
||||||
const persistSettings = useCallback(
|
const persistSettings = useCallback(
|
||||||
async (projectPath: string, settingsToUpdate: Record<string, unknown>) => {
|
(projectPath: string, settingsToUpdate: Record<string, unknown>) => {
|
||||||
try {
|
updateProjectSettings.mutate({
|
||||||
const result = await httpClient.settings.updateProject(projectPath, {
|
projectPath,
|
||||||
boardBackground: settingsToUpdate,
|
settings: { boardBackground: settingsToUpdate },
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!result.success) {
|
|
||||||
logger.error('Failed to persist settings:', result.error);
|
|
||||||
toast.error('Failed to save settings');
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
logger.error('Failed to persist settings:', error);
|
|
||||||
toast.error('Failed to save settings');
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
[httpClient]
|
[updateProjectSettings]
|
||||||
);
|
);
|
||||||
|
|
||||||
// Get current background settings for a project
|
// Get current background settings for a project
|
||||||
|
|||||||
@@ -2,12 +2,12 @@
|
|||||||
* Hook for fetching guided prompts from the backend API
|
* Hook for fetching guided prompts from the backend API
|
||||||
*
|
*
|
||||||
* This hook provides the single source of truth for guided prompts,
|
* This hook provides the single source of truth for guided prompts,
|
||||||
* fetched from the backend /api/ideation/prompts endpoint.
|
* with caching via React Query.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { useState, useEffect, useCallback } from 'react';
|
import { useCallback, useMemo } from 'react';
|
||||||
import type { IdeationPrompt, PromptCategory, IdeaCategory } from '@automaker/types';
|
import type { IdeationPrompt, PromptCategory, IdeaCategory } from '@automaker/types';
|
||||||
import { getElectronAPI } from '@/lib/electron';
|
import { useIdeationPrompts } from '@/hooks/queries';
|
||||||
|
|
||||||
interface UseGuidedPromptsReturn {
|
interface UseGuidedPromptsReturn {
|
||||||
prompts: IdeationPrompt[];
|
prompts: IdeationPrompt[];
|
||||||
@@ -21,36 +21,10 @@ interface UseGuidedPromptsReturn {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function useGuidedPrompts(): UseGuidedPromptsReturn {
|
export function useGuidedPrompts(): UseGuidedPromptsReturn {
|
||||||
const [prompts, setPrompts] = useState<IdeationPrompt[]>([]);
|
const { data, isLoading, error, refetch } = useIdeationPrompts();
|
||||||
const [categories, setCategories] = useState<PromptCategory[]>([]);
|
|
||||||
const [isLoading, setIsLoading] = useState(true);
|
|
||||||
const [error, setError] = useState<string | null>(null);
|
|
||||||
|
|
||||||
const fetchPrompts = useCallback(async () => {
|
const prompts = data?.prompts ?? [];
|
||||||
setIsLoading(true);
|
const categories = data?.categories ?? [];
|
||||||
setError(null);
|
|
||||||
|
|
||||||
try {
|
|
||||||
const api = getElectronAPI();
|
|
||||||
const result = await api.ideation?.getPrompts();
|
|
||||||
|
|
||||||
if (result?.success) {
|
|
||||||
setPrompts(result.prompts || []);
|
|
||||||
setCategories(result.categories || []);
|
|
||||||
} else {
|
|
||||||
setError(result?.error || 'Failed to fetch prompts');
|
|
||||||
}
|
|
||||||
} catch (err) {
|
|
||||||
console.error('Failed to fetch guided prompts:', err);
|
|
||||||
setError(err instanceof Error ? err.message : 'Failed to fetch prompts');
|
|
||||||
} finally {
|
|
||||||
setIsLoading(false);
|
|
||||||
}
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
fetchPrompts();
|
|
||||||
}, [fetchPrompts]);
|
|
||||||
|
|
||||||
const getPromptsByCategory = useCallback(
|
const getPromptsByCategory = useCallback(
|
||||||
(category: IdeaCategory): IdeationPrompt[] => {
|
(category: IdeaCategory): IdeationPrompt[] => {
|
||||||
@@ -73,12 +47,23 @@ export function useGuidedPrompts(): UseGuidedPromptsReturn {
|
|||||||
[categories]
|
[categories]
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// Convert async refetch to match the expected interface
|
||||||
|
const handleRefetch = useCallback(async () => {
|
||||||
|
await refetch();
|
||||||
|
}, [refetch]);
|
||||||
|
|
||||||
|
// Convert error to string for backward compatibility
|
||||||
|
const errorMessage = useMemo(() => {
|
||||||
|
if (!error) return null;
|
||||||
|
return error instanceof Error ? error.message : String(error);
|
||||||
|
}, [error]);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
prompts,
|
prompts,
|
||||||
categories,
|
categories,
|
||||||
isLoading,
|
isLoading,
|
||||||
error,
|
error: errorMessage,
|
||||||
refetch: fetchPrompts,
|
refetch: handleRefetch,
|
||||||
getPromptsByCategory,
|
getPromptsByCategory,
|
||||||
getPromptById,
|
getPromptById,
|
||||||
getCategoryById,
|
getCategoryById,
|
||||||
|
|||||||
@@ -1,11 +1,13 @@
|
|||||||
import { useEffect, useRef } from 'react';
|
import { useEffect, useRef } from 'react';
|
||||||
import { useAppStore } from '@/store/app-store';
|
import { useAppStore } from '@/store/app-store';
|
||||||
import { getHttpApiClient } from '@/lib/http-api-client';
|
import { useProjectSettings } from '@/hooks/queries';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Hook that loads project settings from the server when the current project changes.
|
* Hook that loads project settings from the server when the current project changes.
|
||||||
* This ensures that settings like board backgrounds are properly restored when
|
* This ensures that settings like board backgrounds are properly restored when
|
||||||
* switching between projects or restarting the app.
|
* switching between projects or restarting the app.
|
||||||
|
*
|
||||||
|
* Uses React Query for data fetching with automatic caching.
|
||||||
*/
|
*/
|
||||||
export function useProjectSettingsLoader() {
|
export function useProjectSettingsLoader() {
|
||||||
const currentProject = useAppStore((state) => state.currentProject);
|
const currentProject = useAppStore((state) => state.currentProject);
|
||||||
@@ -24,93 +26,84 @@ export function useProjectSettingsLoader() {
|
|||||||
(state) => state.setAutoDismissInitScriptIndicator
|
(state) => state.setAutoDismissInitScriptIndicator
|
||||||
);
|
);
|
||||||
|
|
||||||
const loadingRef = useRef<string | null>(null);
|
const appliedProjectRef = useRef<string | null>(null);
|
||||||
const currentProjectRef = useRef<string | null>(null);
|
|
||||||
|
|
||||||
|
// Fetch project settings with React Query
|
||||||
|
const { data: settings } = useProjectSettings(currentProject?.path);
|
||||||
|
|
||||||
|
// Apply settings when data changes
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
currentProjectRef.current = currentProject?.path ?? null;
|
if (!currentProject?.path || !settings) {
|
||||||
|
|
||||||
if (!currentProject?.path) {
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Prevent loading the same project multiple times
|
// Prevent applying the same settings multiple times
|
||||||
if (loadingRef.current === currentProject.path) {
|
if (appliedProjectRef.current === currentProject.path) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
loadingRef.current = currentProject.path;
|
appliedProjectRef.current = currentProject.path;
|
||||||
const requestedProjectPath = currentProject.path;
|
const projectPath = currentProject.path;
|
||||||
|
|
||||||
const loadProjectSettings = async () => {
|
const bg = settings.boardBackground;
|
||||||
try {
|
|
||||||
const httpClient = getHttpApiClient();
|
|
||||||
const result = await httpClient.settings.getProject(requestedProjectPath);
|
|
||||||
|
|
||||||
// Race condition protection: ignore stale results if project changed
|
// Apply boardBackground if present
|
||||||
if (currentProjectRef.current !== requestedProjectPath) {
|
if (bg?.imagePath) {
|
||||||
return;
|
setBoardBackground(projectPath, bg.imagePath);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (result.success && result.settings) {
|
// Settings map for cleaner iteration
|
||||||
const bg = result.settings.boardBackground;
|
const settingsMap = {
|
||||||
|
cardOpacity: setCardOpacity,
|
||||||
|
columnOpacity: setColumnOpacity,
|
||||||
|
columnBorderEnabled: setColumnBorderEnabled,
|
||||||
|
cardGlassmorphism: setCardGlassmorphism,
|
||||||
|
cardBorderEnabled: setCardBorderEnabled,
|
||||||
|
cardBorderOpacity: setCardBorderOpacity,
|
||||||
|
hideScrollbar: setHideScrollbar,
|
||||||
|
} as const;
|
||||||
|
|
||||||
// Apply boardBackground if present
|
// Apply all settings that are defined
|
||||||
if (bg?.imagePath) {
|
for (const [key, setter] of Object.entries(settingsMap)) {
|
||||||
setBoardBackground(requestedProjectPath, bg.imagePath);
|
const value = bg?.[key as keyof typeof bg];
|
||||||
}
|
if (value !== undefined) {
|
||||||
|
(setter as (path: string, val: typeof value) => void)(projectPath, value);
|
||||||
// Settings map for cleaner iteration
|
|
||||||
const settingsMap = {
|
|
||||||
cardOpacity: setCardOpacity,
|
|
||||||
columnOpacity: setColumnOpacity,
|
|
||||||
columnBorderEnabled: setColumnBorderEnabled,
|
|
||||||
cardGlassmorphism: setCardGlassmorphism,
|
|
||||||
cardBorderEnabled: setCardBorderEnabled,
|
|
||||||
cardBorderOpacity: setCardBorderOpacity,
|
|
||||||
hideScrollbar: setHideScrollbar,
|
|
||||||
} as const;
|
|
||||||
|
|
||||||
// Apply all settings that are defined
|
|
||||||
for (const [key, setter] of Object.entries(settingsMap)) {
|
|
||||||
const value = bg?.[key as keyof typeof bg];
|
|
||||||
if (value !== undefined) {
|
|
||||||
(setter as (path: string, val: typeof value) => void)(requestedProjectPath, value);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Apply worktreePanelVisible if present
|
|
||||||
if (result.settings.worktreePanelVisible !== undefined) {
|
|
||||||
setWorktreePanelVisible(requestedProjectPath, result.settings.worktreePanelVisible);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Apply showInitScriptIndicator if present
|
|
||||||
if (result.settings.showInitScriptIndicator !== undefined) {
|
|
||||||
setShowInitScriptIndicator(
|
|
||||||
requestedProjectPath,
|
|
||||||
result.settings.showInitScriptIndicator
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Apply defaultDeleteBranch if present
|
|
||||||
if (result.settings.defaultDeleteBranch !== undefined) {
|
|
||||||
setDefaultDeleteBranch(requestedProjectPath, result.settings.defaultDeleteBranch);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Apply autoDismissInitScriptIndicator if present
|
|
||||||
if (result.settings.autoDismissInitScriptIndicator !== undefined) {
|
|
||||||
setAutoDismissInitScriptIndicator(
|
|
||||||
requestedProjectPath,
|
|
||||||
result.settings.autoDismissInitScriptIndicator
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Failed to load project settings:', error);
|
|
||||||
// Don't show error toast - just log it
|
|
||||||
}
|
}
|
||||||
};
|
}
|
||||||
|
|
||||||
loadProjectSettings();
|
// Apply worktreePanelVisible if present
|
||||||
}, [currentProject?.path]);
|
if (settings.worktreePanelVisible !== undefined) {
|
||||||
|
setWorktreePanelVisible(projectPath, settings.worktreePanelVisible);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Apply showInitScriptIndicator if present
|
||||||
|
if (settings.showInitScriptIndicator !== undefined) {
|
||||||
|
setShowInitScriptIndicator(projectPath, settings.showInitScriptIndicator);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Apply defaultDeleteBranchWithWorktree if present
|
||||||
|
if (settings.defaultDeleteBranchWithWorktree !== undefined) {
|
||||||
|
setDefaultDeleteBranch(projectPath, settings.defaultDeleteBranchWithWorktree);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Apply autoDismissInitScriptIndicator if present
|
||||||
|
if (settings.autoDismissInitScriptIndicator !== undefined) {
|
||||||
|
setAutoDismissInitScriptIndicator(projectPath, settings.autoDismissInitScriptIndicator);
|
||||||
|
}
|
||||||
|
}, [
|
||||||
|
currentProject?.path,
|
||||||
|
settings,
|
||||||
|
setBoardBackground,
|
||||||
|
setCardOpacity,
|
||||||
|
setColumnOpacity,
|
||||||
|
setColumnBorderEnabled,
|
||||||
|
setCardGlassmorphism,
|
||||||
|
setCardBorderEnabled,
|
||||||
|
setCardBorderOpacity,
|
||||||
|
setHideScrollbar,
|
||||||
|
setWorktreePanelVisible,
|
||||||
|
setShowInitScriptIndicator,
|
||||||
|
setDefaultDeleteBranch,
|
||||||
|
setAutoDismissInitScriptIndicator,
|
||||||
|
]);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user