mirror of
https://github.com/AutoMaker-Org/automaker.git
synced 2026-01-30 06:12:03 +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 {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
@@ -9,7 +8,7 @@ import {
|
||||
} from '@/components/ui/dialog';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Folder, Loader2, FolderOpen, AlertCircle } from 'lucide-react';
|
||||
import { getHttpApiClient } from '@/lib/http-api-client';
|
||||
import { useWorkspaceDirectories } from '@/hooks/queries';
|
||||
|
||||
interface WorkspaceDirectory {
|
||||
name: string;
|
||||
@@ -23,41 +22,15 @@ interface WorkspacePickerModalProps {
|
||||
}
|
||||
|
||||
export function WorkspacePickerModal({ open, onOpenChange, onSelect }: WorkspacePickerModalProps) {
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [directories, setDirectories] = useState<WorkspaceDirectory[]>([]);
|
||||
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]);
|
||||
// React Query hook - only fetch when modal is open
|
||||
const { data: directories = [], isLoading, error, refetch } = useWorkspaceDirectories(open);
|
||||
|
||||
const handleSelect = (dir: WorkspaceDirectory) => {
|
||||
onSelect(dir.path, dir.name);
|
||||
};
|
||||
|
||||
const errorMessage = error instanceof Error ? error.message : null;
|
||||
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={onOpenChange}>
|
||||
<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>
|
||||
)}
|
||||
|
||||
{error && !isLoading && (
|
||||
{errorMessage && !isLoading && (
|
||||
<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">
|
||||
<AlertCircle className="w-6 h-6 text-destructive" />
|
||||
</div>
|
||||
<p className="text-sm text-destructive">{error}</p>
|
||||
<Button variant="secondary" size="sm" onClick={loadDirectories} className="mt-2">
|
||||
<p className="text-sm text-destructive">{errorMessage}</p>
|
||||
<Button variant="secondary" size="sm" onClick={() => refetch()} className="mt-2">
|
||||
Try Again
|
||||
</Button>
|
||||
</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="w-12 h-12 rounded-full bg-muted flex items-center justify-center">
|
||||
<Folder className="w-6 h-6 text-muted-foreground" />
|
||||
@@ -102,7 +75,7 @@ export function WorkspacePickerModal({ open, onOpenChange, onSelect }: Workspace
|
||||
</div>
|
||||
)}
|
||||
|
||||
{!isLoading && !error && directories.length > 0 && (
|
||||
{!isLoading && !errorMessage && directories.length > 0 && (
|
||||
<div className="space-y-2">
|
||||
{directories.map((dir) => (
|
||||
<button
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { useState, useEffect } from 'react';
|
||||
import { useState, useEffect, useCallback } from 'react';
|
||||
import { createLogger } from '@automaker/utils/logger';
|
||||
import { useQueryClient } from '@tanstack/react-query';
|
||||
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
|
||||
|
||||
const logger = createLogger('SessionManager');
|
||||
@@ -22,6 +23,8 @@ import { cn } from '@/lib/utils';
|
||||
import type { SessionListItem } from '@/types/electron';
|
||||
import { useKeyboardShortcutsConfig } from '@/hooks/use-keyboard-shortcuts';
|
||||
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 { DeleteAllArchivedSessionsDialog } from '@/components/dialogs/delete-all-archived-sessions-dialog';
|
||||
|
||||
@@ -102,7 +105,7 @@ export function SessionManager({
|
||||
onQuickCreateRef,
|
||||
}: SessionManagerProps) {
|
||||
const shortcuts = useKeyboardShortcutsConfig();
|
||||
const [sessions, setSessions] = useState<SessionListItem[]>([]);
|
||||
const queryClient = useQueryClient();
|
||||
const [activeTab, setActiveTab] = useState<'active' | 'archived'>('active');
|
||||
const [editingSessionId, setEditingSessionId] = useState<string | null>(null);
|
||||
const [editingName, setEditingName] = useState('');
|
||||
@@ -113,8 +116,11 @@ export function SessionManager({
|
||||
const [sessionToDelete, setSessionToDelete] = useState<SessionListItem | null>(null);
|
||||
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
|
||||
const checkRunningSessions = async (sessionList: SessionListItem[]) => {
|
||||
const checkRunningSessions = useCallback(async (sessionList: SessionListItem[]) => {
|
||||
const api = getElectronAPI();
|
||||
if (!api?.agent) return;
|
||||
|
||||
@@ -134,26 +140,25 @@ export function SessionManager({
|
||||
}
|
||||
|
||||
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)
|
||||
useEffect(() => {
|
||||
// Only poll if there are running sessions
|
||||
@@ -166,7 +171,7 @@ export function SessionManager({
|
||||
}, 3000); // Check every 3 seconds
|
||||
|
||||
return () => clearInterval(interval);
|
||||
}, [sessions, runningSessions.size, isCurrentSessionThinking]);
|
||||
}, [sessions, runningSessions.size, isCurrentSessionThinking, checkRunningSessions]);
|
||||
|
||||
// Create new session with random name
|
||||
const handleCreateSession = async () => {
|
||||
@@ -180,7 +185,7 @@ export function SessionManager({
|
||||
if (result.success && result.session?.id) {
|
||||
setNewSessionName('');
|
||||
setIsCreating(false);
|
||||
await loadSessions();
|
||||
await invalidateSessions();
|
||||
onSelectSession(result.session.id);
|
||||
}
|
||||
};
|
||||
@@ -195,7 +200,7 @@ export function SessionManager({
|
||||
const result = await api.sessions.create(sessionName, projectPath, projectPath);
|
||||
|
||||
if (result.success && result.session?.id) {
|
||||
await loadSessions();
|
||||
await invalidateSessions();
|
||||
onSelectSession(result.session.id);
|
||||
}
|
||||
};
|
||||
@@ -222,7 +227,7 @@ export function SessionManager({
|
||||
if (result.success) {
|
||||
setEditingSessionId(null);
|
||||
setEditingName('');
|
||||
await loadSessions();
|
||||
await invalidateSessions();
|
||||
}
|
||||
};
|
||||
|
||||
@@ -241,7 +246,7 @@ export function SessionManager({
|
||||
if (currentSessionId === sessionId) {
|
||||
onSelectSession(null);
|
||||
}
|
||||
await loadSessions();
|
||||
await invalidateSessions();
|
||||
} else {
|
||||
logger.error('[SessionManager] Archive failed:', result.error);
|
||||
}
|
||||
@@ -261,7 +266,7 @@ export function SessionManager({
|
||||
try {
|
||||
const result = await api.sessions.unarchive(sessionId);
|
||||
if (result.success) {
|
||||
await loadSessions();
|
||||
await invalidateSessions();
|
||||
} else {
|
||||
logger.error('[SessionManager] Unarchive failed:', result.error);
|
||||
}
|
||||
@@ -283,7 +288,7 @@ export function SessionManager({
|
||||
|
||||
const result = await api.sessions.delete(sessionId);
|
||||
if (result.success) {
|
||||
await loadSessions();
|
||||
await invalidateSessions();
|
||||
if (currentSessionId === sessionId) {
|
||||
// Switch to another session or create a new one
|
||||
const activeSessionsList = sessions.filter((s) => !s.isArchived);
|
||||
@@ -305,7 +310,7 @@ export function SessionManager({
|
||||
await api.sessions.delete(session.id);
|
||||
}
|
||||
|
||||
await loadSessions();
|
||||
await invalidateSessions();
|
||||
setIsDeleteAllArchivedDialogOpen(false);
|
||||
};
|
||||
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import { useState, useEffect, useMemo, useCallback } from 'react';
|
||||
import { getElectronAPI } from '@/lib/electron';
|
||||
import { useState, useMemo } from 'react';
|
||||
import { cn } from '@/lib/utils';
|
||||
import {
|
||||
File,
|
||||
@@ -15,6 +14,7 @@ import {
|
||||
AlertCircle,
|
||||
} from 'lucide-react';
|
||||
import { Button } from './button';
|
||||
import { useWorktreeDiffs, useGitDiffs } from '@/hooks/queries';
|
||||
import type { FileStatus } from '@/types/electron';
|
||||
|
||||
interface GitDiffPanelProps {
|
||||
@@ -350,56 +350,44 @@ export function GitDiffPanel({
|
||||
useWorktrees = false,
|
||||
}: GitDiffPanelProps) {
|
||||
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 loadDiffs = useCallback(async () => {
|
||||
setIsLoading(true);
|
||||
setError(null);
|
||||
try {
|
||||
const api = getElectronAPI();
|
||||
// Use worktree diffs hook when worktrees are enabled and panel is expanded
|
||||
// Pass undefined for featureId when not using worktrees to disable the query
|
||||
const {
|
||||
data: worktreeDiffsData,
|
||||
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
|
||||
if (useWorktrees) {
|
||||
if (!api?.worktree?.getDiffs) {
|
||||
throw new Error('Worktree API not available');
|
||||
}
|
||||
const result = await api.worktree.getDiffs(projectPath, featureId);
|
||||
if (result.success) {
|
||||
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]);
|
||||
// Use git diffs hook when worktrees are disabled and panel is expanded
|
||||
const {
|
||||
data: gitDiffsData,
|
||||
isLoading: isLoadingGit,
|
||||
error: gitError,
|
||||
refetch: refetchGit,
|
||||
} = useGitDiffs(projectPath, !useWorktrees && isExpanded);
|
||||
|
||||
// Load diffs when expanded
|
||||
useEffect(() => {
|
||||
if (isExpanded) {
|
||||
loadDiffs();
|
||||
}
|
||||
}, [isExpanded, loadDiffs]);
|
||||
// Select the appropriate data based on useWorktrees prop
|
||||
const diffsData = useWorktrees ? worktreeDiffsData : gitDiffsData;
|
||||
const isLoading = useWorktrees ? isLoadingWorktree : isLoadingGit;
|
||||
const queryError = useWorktrees ? worktreeError : gitError;
|
||||
|
||||
// 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]);
|
||||
|
||||
|
||||
@@ -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 (
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -1,36 +1,26 @@
|
||||
import { useCallback } from 'react';
|
||||
import { createLogger } from '@automaker/utils/logger';
|
||||
import { useAppStore } from '@/store/app-store';
|
||||
import { getHttpApiClient } from '@/lib/http-api-client';
|
||||
import { toast } from 'sonner';
|
||||
|
||||
const logger = createLogger('BoardBackground');
|
||||
import { useUpdateProjectSettings } from '@/hooks/mutations';
|
||||
|
||||
/**
|
||||
* 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() {
|
||||
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
|
||||
const persistSettings = useCallback(
|
||||
async (projectPath: string, settingsToUpdate: Record<string, unknown>) => {
|
||||
try {
|
||||
const result = await httpClient.settings.updateProject(projectPath, {
|
||||
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');
|
||||
}
|
||||
(projectPath: string, settingsToUpdate: Record<string, unknown>) => {
|
||||
updateProjectSettings.mutate({
|
||||
projectPath,
|
||||
settings: { boardBackground: settingsToUpdate },
|
||||
});
|
||||
},
|
||||
[httpClient]
|
||||
[updateProjectSettings]
|
||||
);
|
||||
|
||||
// Get current background settings for a project
|
||||
|
||||
@@ -2,12 +2,12 @@
|
||||
* Hook for fetching guided prompts from the backend API
|
||||
*
|
||||
* 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 { getElectronAPI } from '@/lib/electron';
|
||||
import { useIdeationPrompts } from '@/hooks/queries';
|
||||
|
||||
interface UseGuidedPromptsReturn {
|
||||
prompts: IdeationPrompt[];
|
||||
@@ -21,36 +21,10 @@ interface UseGuidedPromptsReturn {
|
||||
}
|
||||
|
||||
export function useGuidedPrompts(): UseGuidedPromptsReturn {
|
||||
const [prompts, setPrompts] = useState<IdeationPrompt[]>([]);
|
||||
const [categories, setCategories] = useState<PromptCategory[]>([]);
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const { data, isLoading, error, refetch } = useIdeationPrompts();
|
||||
|
||||
const fetchPrompts = useCallback(async () => {
|
||||
setIsLoading(true);
|
||||
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 prompts = data?.prompts ?? [];
|
||||
const categories = data?.categories ?? [];
|
||||
|
||||
const getPromptsByCategory = useCallback(
|
||||
(category: IdeaCategory): IdeationPrompt[] => {
|
||||
@@ -73,12 +47,23 @@ export function useGuidedPrompts(): UseGuidedPromptsReturn {
|
||||
[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 {
|
||||
prompts,
|
||||
categories,
|
||||
isLoading,
|
||||
error,
|
||||
refetch: fetchPrompts,
|
||||
error: errorMessage,
|
||||
refetch: handleRefetch,
|
||||
getPromptsByCategory,
|
||||
getPromptById,
|
||||
getCategoryById,
|
||||
|
||||
@@ -1,11 +1,13 @@
|
||||
import { useEffect, useRef } from 'react';
|
||||
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.
|
||||
* This ensures that settings like board backgrounds are properly restored when
|
||||
* switching between projects or restarting the app.
|
||||
*
|
||||
* Uses React Query for data fetching with automatic caching.
|
||||
*/
|
||||
export function useProjectSettingsLoader() {
|
||||
const currentProject = useAppStore((state) => state.currentProject);
|
||||
@@ -24,93 +26,84 @@ export function useProjectSettingsLoader() {
|
||||
(state) => state.setAutoDismissInitScriptIndicator
|
||||
);
|
||||
|
||||
const loadingRef = useRef<string | null>(null);
|
||||
const currentProjectRef = useRef<string | null>(null);
|
||||
const appliedProjectRef = useRef<string | null>(null);
|
||||
|
||||
// Fetch project settings with React Query
|
||||
const { data: settings } = useProjectSettings(currentProject?.path);
|
||||
|
||||
// Apply settings when data changes
|
||||
useEffect(() => {
|
||||
currentProjectRef.current = currentProject?.path ?? null;
|
||||
|
||||
if (!currentProject?.path) {
|
||||
if (!currentProject?.path || !settings) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Prevent loading the same project multiple times
|
||||
if (loadingRef.current === currentProject.path) {
|
||||
// Prevent applying the same settings multiple times
|
||||
if (appliedProjectRef.current === currentProject.path) {
|
||||
return;
|
||||
}
|
||||
|
||||
loadingRef.current = currentProject.path;
|
||||
const requestedProjectPath = currentProject.path;
|
||||
appliedProjectRef.current = currentProject.path;
|
||||
const projectPath = currentProject.path;
|
||||
|
||||
const loadProjectSettings = async () => {
|
||||
try {
|
||||
const httpClient = getHttpApiClient();
|
||||
const result = await httpClient.settings.getProject(requestedProjectPath);
|
||||
const bg = settings.boardBackground;
|
||||
|
||||
// Race condition protection: ignore stale results if project changed
|
||||
if (currentProjectRef.current !== requestedProjectPath) {
|
||||
return;
|
||||
}
|
||||
// Apply boardBackground if present
|
||||
if (bg?.imagePath) {
|
||||
setBoardBackground(projectPath, bg.imagePath);
|
||||
}
|
||||
|
||||
if (result.success && result.settings) {
|
||||
const bg = result.settings.boardBackground;
|
||||
// Settings map for cleaner iteration
|
||||
const settingsMap = {
|
||||
cardOpacity: setCardOpacity,
|
||||
columnOpacity: setColumnOpacity,
|
||||
columnBorderEnabled: setColumnBorderEnabled,
|
||||
cardGlassmorphism: setCardGlassmorphism,
|
||||
cardBorderEnabled: setCardBorderEnabled,
|
||||
cardBorderOpacity: setCardBorderOpacity,
|
||||
hideScrollbar: setHideScrollbar,
|
||||
} as const;
|
||||
|
||||
// Apply boardBackground if present
|
||||
if (bg?.imagePath) {
|
||||
setBoardBackground(requestedProjectPath, bg.imagePath);
|
||||
}
|
||||
|
||||
// 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
|
||||
// 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)(projectPath, value);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
loadProjectSettings();
|
||||
}, [currentProject?.path]);
|
||||
// Apply worktreePanelVisible if present
|
||||
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