mirror of
https://github.com/AutoMaker-Org/automaker.git
synced 2026-01-30 06:12:03 +00:00
refactor(ui): migrate worktree panel to React Query
- Migrate use-worktrees to useWorktrees query hook - Migrate use-branches to useWorktreeBranches query hook - Migrate use-available-editors to useAvailableEditors query hook - Migrate use-worktree-actions to use mutation hooks - Update worktree-panel component to use query data - Remove manual state management for loading/errors Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -1,65 +1,46 @@
|
||||
import { useState, useEffect, useCallback, useMemo } from 'react';
|
||||
import { createLogger } from '@automaker/utils/logger';
|
||||
import { useMemo, useCallback } from 'react';
|
||||
import { useMutation, useQueryClient } from '@tanstack/react-query';
|
||||
import { getElectronAPI } from '@/lib/electron';
|
||||
import { useAppStore } from '@/store/app-store';
|
||||
import { useAvailableEditors as useAvailableEditorsQuery } from '@/hooks/queries';
|
||||
import { queryKeys } from '@/lib/query-keys';
|
||||
import type { EditorInfo } from '@automaker/types';
|
||||
|
||||
const logger = createLogger('AvailableEditors');
|
||||
|
||||
// Re-export EditorInfo for convenience
|
||||
export type { EditorInfo };
|
||||
|
||||
/**
|
||||
* Hook for fetching and managing available editors
|
||||
*
|
||||
* Uses React Query for data fetching with caching.
|
||||
* Provides a refresh function that clears server cache and re-detects editors.
|
||||
*/
|
||||
export function useAvailableEditors() {
|
||||
const [editors, setEditors] = useState<EditorInfo[]>([]);
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
const [isRefreshing, setIsRefreshing] = useState(false);
|
||||
|
||||
const fetchAvailableEditors = useCallback(async () => {
|
||||
try {
|
||||
const api = getElectronAPI();
|
||||
if (!api?.worktree?.getAvailableEditors) {
|
||||
setIsLoading(false);
|
||||
return;
|
||||
}
|
||||
const result = await api.worktree.getAvailableEditors();
|
||||
if (result.success && result.result?.editors) {
|
||||
setEditors(result.result.editors);
|
||||
}
|
||||
} catch (error) {
|
||||
logger.error('Failed to fetch available editors:', error);
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
}, []);
|
||||
const queryClient = useQueryClient();
|
||||
const { data: editors = [], isLoading } = useAvailableEditorsQuery();
|
||||
|
||||
/**
|
||||
* Refresh editors by clearing the server cache and re-detecting
|
||||
* Mutation to refresh editors by clearing the server cache and re-detecting
|
||||
* Use this when the user has installed/uninstalled editors
|
||||
*/
|
||||
const refresh = useCallback(async () => {
|
||||
setIsRefreshing(true);
|
||||
try {
|
||||
const { mutate: refreshMutate, isPending: isRefreshing } = useMutation({
|
||||
mutationFn: async () => {
|
||||
const api = getElectronAPI();
|
||||
if (!api?.worktree?.refreshEditors) {
|
||||
// Fallback to regular fetch if refresh not available
|
||||
await fetchAvailableEditors();
|
||||
return;
|
||||
}
|
||||
const result = await api.worktree.refreshEditors();
|
||||
if (result.success && result.result?.editors) {
|
||||
setEditors(result.result.editors);
|
||||
logger.info(`Editor cache refreshed, found ${result.result.editors.length} editors`);
|
||||
if (!result.success) {
|
||||
throw new Error(result.error || 'Failed to refresh editors');
|
||||
}
|
||||
} catch (error) {
|
||||
logger.error('Failed to refresh editors:', error);
|
||||
} finally {
|
||||
setIsRefreshing(false);
|
||||
}
|
||||
}, [fetchAvailableEditors]);
|
||||
return result.result?.editors ?? [];
|
||||
},
|
||||
onSuccess: (newEditors) => {
|
||||
// Update the cache with new editors
|
||||
queryClient.setQueryData(queryKeys.worktrees.editors(), newEditors);
|
||||
},
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
fetchAvailableEditors();
|
||||
}, [fetchAvailableEditors]);
|
||||
const refresh = useCallback(() => {
|
||||
refreshMutate();
|
||||
}, [refreshMutate]);
|
||||
|
||||
return {
|
||||
editors,
|
||||
|
||||
@@ -1,66 +1,43 @@
|
||||
import { useState, useCallback } from 'react';
|
||||
import { createLogger } from '@automaker/utils/logger';
|
||||
import { getElectronAPI } from '@/lib/electron';
|
||||
import type { BranchInfo, GitRepoStatus } from '../types';
|
||||
|
||||
const logger = createLogger('Branches');
|
||||
import { useWorktreeBranches } from '@/hooks/queries';
|
||||
import type { GitRepoStatus } from '../types';
|
||||
|
||||
/**
|
||||
* Hook for managing branch data with React Query
|
||||
*
|
||||
* Uses useWorktreeBranches for data fetching while maintaining
|
||||
* the current interface for backward compatibility. Tracks which
|
||||
* worktree path is currently being viewed and fetches branches on demand.
|
||||
*/
|
||||
export function useBranches() {
|
||||
const [branches, setBranches] = useState<BranchInfo[]>([]);
|
||||
const [aheadCount, setAheadCount] = useState(0);
|
||||
const [behindCount, setBehindCount] = useState(0);
|
||||
const [isLoadingBranches, setIsLoadingBranches] = useState(false);
|
||||
const [currentWorktreePath, setCurrentWorktreePath] = useState<string | undefined>();
|
||||
const [branchFilter, setBranchFilter] = useState('');
|
||||
const [gitRepoStatus, setGitRepoStatus] = useState<GitRepoStatus>({
|
||||
isGitRepo: true,
|
||||
hasCommits: true,
|
||||
});
|
||||
|
||||
/** Helper to reset branch state to initial values */
|
||||
const resetBranchState = useCallback(() => {
|
||||
setBranches([]);
|
||||
setAheadCount(0);
|
||||
setBehindCount(0);
|
||||
}, []);
|
||||
const {
|
||||
data: branchData,
|
||||
isLoading: isLoadingBranches,
|
||||
refetch,
|
||||
} = useWorktreeBranches(currentWorktreePath);
|
||||
|
||||
const branches = branchData?.branches ?? [];
|
||||
const aheadCount = branchData?.aheadCount ?? 0;
|
||||
const behindCount = branchData?.behindCount ?? 0;
|
||||
const gitRepoStatus: GitRepoStatus = {
|
||||
isGitRepo: branchData?.isGitRepo ?? true,
|
||||
hasCommits: branchData?.hasCommits ?? true,
|
||||
};
|
||||
|
||||
const fetchBranches = useCallback(
|
||||
async (worktreePath: string) => {
|
||||
setIsLoadingBranches(true);
|
||||
try {
|
||||
const api = getElectronAPI();
|
||||
if (!api?.worktree?.listBranches) {
|
||||
logger.warn('List branches API not available');
|
||||
return;
|
||||
}
|
||||
const result = await api.worktree.listBranches(worktreePath);
|
||||
if (result.success && result.result) {
|
||||
setBranches(result.result.branches);
|
||||
setAheadCount(result.result.aheadCount || 0);
|
||||
setBehindCount(result.result.behindCount || 0);
|
||||
setGitRepoStatus({ isGitRepo: true, hasCommits: true });
|
||||
} else if (result.code === 'NOT_GIT_REPO') {
|
||||
// Not a git repository - clear branches silently without logging an error
|
||||
resetBranchState();
|
||||
setGitRepoStatus({ isGitRepo: false, hasCommits: false });
|
||||
} else if (result.code === 'NO_COMMITS') {
|
||||
// Git repo but no commits yet - clear branches silently without logging an error
|
||||
resetBranchState();
|
||||
setGitRepoStatus({ isGitRepo: true, hasCommits: false });
|
||||
} else if (!result.success) {
|
||||
// Other errors - log them
|
||||
logger.warn('Failed to fetch branches:', result.error);
|
||||
resetBranchState();
|
||||
}
|
||||
} catch (error) {
|
||||
logger.error('Failed to fetch branches:', error);
|
||||
resetBranchState();
|
||||
// Reset git status to unknown state on network/API errors
|
||||
setGitRepoStatus({ isGitRepo: true, hasCommits: true });
|
||||
} finally {
|
||||
setIsLoadingBranches(false);
|
||||
(worktreePath: string) => {
|
||||
if (worktreePath === currentWorktreePath) {
|
||||
// Same path - just refetch to get latest data
|
||||
refetch();
|
||||
} else {
|
||||
// Different path - update the tracked path (triggers new query)
|
||||
setCurrentWorktreePath(worktreePath);
|
||||
}
|
||||
},
|
||||
[resetBranchState]
|
||||
[currentWorktreePath, refetch]
|
||||
);
|
||||
|
||||
const resetBranchFilter = useCallback(() => {
|
||||
|
||||
@@ -1,152 +1,64 @@
|
||||
import { useState, useCallback } from 'react';
|
||||
import { createLogger } from '@automaker/utils/logger';
|
||||
import { getElectronAPI } from '@/lib/electron';
|
||||
import { toast } from 'sonner';
|
||||
import {
|
||||
useSwitchBranch,
|
||||
usePullWorktree,
|
||||
usePushWorktree,
|
||||
useOpenInEditor,
|
||||
} from '@/hooks/mutations';
|
||||
import type { WorktreeInfo } from '../types';
|
||||
|
||||
const logger = createLogger('WorktreeActions');
|
||||
|
||||
// Error codes that need special user-friendly handling
|
||||
const GIT_STATUS_ERROR_CODES = ['NOT_GIT_REPO', 'NO_COMMITS'] as const;
|
||||
type GitStatusErrorCode = (typeof GIT_STATUS_ERROR_CODES)[number];
|
||||
|
||||
// User-friendly messages for git status errors
|
||||
const GIT_STATUS_ERROR_MESSAGES: Record<GitStatusErrorCode, string> = {
|
||||
NOT_GIT_REPO: 'This directory is not a git repository',
|
||||
NO_COMMITS: 'Repository has no commits yet. Create an initial commit first.',
|
||||
};
|
||||
|
||||
/**
|
||||
* Helper to handle git status errors with user-friendly messages.
|
||||
* @returns true if the error was a git status error and was handled, false otherwise.
|
||||
*/
|
||||
function handleGitStatusError(result: { code?: string; error?: string }): boolean {
|
||||
const errorCode = result.code as GitStatusErrorCode | undefined;
|
||||
if (errorCode && GIT_STATUS_ERROR_CODES.includes(errorCode)) {
|
||||
toast.info(GIT_STATUS_ERROR_MESSAGES[errorCode] || result.error);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
interface UseWorktreeActionsOptions {
|
||||
fetchWorktrees: () => Promise<Array<{ path: string; branch: string }> | undefined>;
|
||||
fetchBranches: (worktreePath: string) => Promise<void>;
|
||||
}
|
||||
|
||||
export function useWorktreeActions({ fetchWorktrees, fetchBranches }: UseWorktreeActionsOptions) {
|
||||
const [isPulling, setIsPulling] = useState(false);
|
||||
const [isPushing, setIsPushing] = useState(false);
|
||||
const [isSwitching, setIsSwitching] = useState(false);
|
||||
export function useWorktreeActions() {
|
||||
const [isActivating, setIsActivating] = useState(false);
|
||||
|
||||
// Use React Query mutations
|
||||
const switchBranchMutation = useSwitchBranch();
|
||||
const pullMutation = usePullWorktree();
|
||||
const pushMutation = usePushWorktree();
|
||||
const openInEditorMutation = useOpenInEditor();
|
||||
|
||||
const handleSwitchBranch = useCallback(
|
||||
async (worktree: WorktreeInfo, branchName: string) => {
|
||||
if (isSwitching || branchName === worktree.branch) return;
|
||||
setIsSwitching(true);
|
||||
try {
|
||||
const api = getElectronAPI();
|
||||
if (!api?.worktree?.switchBranch) {
|
||||
toast.error('Switch branch API not available');
|
||||
return;
|
||||
}
|
||||
const result = await api.worktree.switchBranch(worktree.path, branchName);
|
||||
if (result.success && result.result) {
|
||||
toast.success(result.result.message);
|
||||
fetchWorktrees();
|
||||
} else {
|
||||
if (handleGitStatusError(result)) return;
|
||||
toast.error(result.error || 'Failed to switch branch');
|
||||
}
|
||||
} catch (error) {
|
||||
logger.error('Switch branch failed:', error);
|
||||
toast.error('Failed to switch branch');
|
||||
} finally {
|
||||
setIsSwitching(false);
|
||||
}
|
||||
if (switchBranchMutation.isPending || branchName === worktree.branch) return;
|
||||
switchBranchMutation.mutate({
|
||||
worktreePath: worktree.path,
|
||||
branchName,
|
||||
});
|
||||
},
|
||||
[isSwitching, fetchWorktrees]
|
||||
[switchBranchMutation]
|
||||
);
|
||||
|
||||
const handlePull = useCallback(
|
||||
async (worktree: WorktreeInfo) => {
|
||||
if (isPulling) return;
|
||||
setIsPulling(true);
|
||||
try {
|
||||
const api = getElectronAPI();
|
||||
if (!api?.worktree?.pull) {
|
||||
toast.error('Pull API not available');
|
||||
return;
|
||||
}
|
||||
const result = await api.worktree.pull(worktree.path);
|
||||
if (result.success && result.result) {
|
||||
toast.success(result.result.message);
|
||||
fetchWorktrees();
|
||||
} else {
|
||||
if (handleGitStatusError(result)) return;
|
||||
toast.error(result.error || 'Failed to pull latest changes');
|
||||
}
|
||||
} catch (error) {
|
||||
logger.error('Pull failed:', error);
|
||||
toast.error('Failed to pull latest changes');
|
||||
} finally {
|
||||
setIsPulling(false);
|
||||
}
|
||||
if (pullMutation.isPending) return;
|
||||
pullMutation.mutate(worktree.path);
|
||||
},
|
||||
[isPulling, fetchWorktrees]
|
||||
[pullMutation]
|
||||
);
|
||||
|
||||
const handlePush = useCallback(
|
||||
async (worktree: WorktreeInfo) => {
|
||||
if (isPushing) return;
|
||||
setIsPushing(true);
|
||||
try {
|
||||
const api = getElectronAPI();
|
||||
if (!api?.worktree?.push) {
|
||||
toast.error('Push API not available');
|
||||
return;
|
||||
}
|
||||
const result = await api.worktree.push(worktree.path);
|
||||
if (result.success && result.result) {
|
||||
toast.success(result.result.message);
|
||||
fetchBranches(worktree.path);
|
||||
fetchWorktrees();
|
||||
} else {
|
||||
if (handleGitStatusError(result)) return;
|
||||
toast.error(result.error || 'Failed to push changes');
|
||||
}
|
||||
} catch (error) {
|
||||
logger.error('Push failed:', error);
|
||||
toast.error('Failed to push changes');
|
||||
} finally {
|
||||
setIsPushing(false);
|
||||
}
|
||||
if (pushMutation.isPending) return;
|
||||
pushMutation.mutate({
|
||||
worktreePath: worktree.path,
|
||||
});
|
||||
},
|
||||
[isPushing, fetchBranches, fetchWorktrees]
|
||||
[pushMutation]
|
||||
);
|
||||
|
||||
const handleOpenInEditor = useCallback(async (worktree: WorktreeInfo, editorCommand?: string) => {
|
||||
try {
|
||||
const api = getElectronAPI();
|
||||
if (!api?.worktree?.openInEditor) {
|
||||
logger.warn('Open in editor API not available');
|
||||
return;
|
||||
}
|
||||
const result = await api.worktree.openInEditor(worktree.path, editorCommand);
|
||||
if (result.success && result.result) {
|
||||
toast.success(result.result.message);
|
||||
} else if (result.error) {
|
||||
toast.error(result.error);
|
||||
}
|
||||
} catch (error) {
|
||||
logger.error('Open in editor failed:', error);
|
||||
}
|
||||
}, []);
|
||||
const handleOpenInEditor = useCallback(
|
||||
async (worktree: WorktreeInfo, editorCommand?: string) => {
|
||||
openInEditorMutation.mutate({
|
||||
worktreePath: worktree.path,
|
||||
editorCommand,
|
||||
});
|
||||
},
|
||||
[openInEditorMutation]
|
||||
);
|
||||
|
||||
return {
|
||||
isPulling,
|
||||
isPushing,
|
||||
isSwitching,
|
||||
isPulling: pullMutation.isPending,
|
||||
isPushing: pushMutation.isPending,
|
||||
isSwitching: switchBranchMutation.isPending,
|
||||
isActivating,
|
||||
setIsActivating,
|
||||
handleSwitchBranch,
|
||||
|
||||
@@ -1,12 +1,11 @@
|
||||
import { useState, useEffect, useCallback, useRef } from 'react';
|
||||
import { createLogger } from '@automaker/utils/logger';
|
||||
import { useEffect, useCallback, useRef } from 'react';
|
||||
import { useQueryClient } from '@tanstack/react-query';
|
||||
import { useAppStore } from '@/store/app-store';
|
||||
import { getElectronAPI } from '@/lib/electron';
|
||||
import { useWorktrees as useWorktreesQuery } from '@/hooks/queries';
|
||||
import { queryKeys } from '@/lib/query-keys';
|
||||
import { pathsEqual } from '@/lib/utils';
|
||||
import type { WorktreeInfo } from '../types';
|
||||
|
||||
const logger = createLogger('Worktrees');
|
||||
|
||||
interface UseWorktreesOptions {
|
||||
projectPath: string;
|
||||
refreshTrigger?: number;
|
||||
@@ -18,59 +17,46 @@ export function useWorktrees({
|
||||
refreshTrigger = 0,
|
||||
onRemovedWorktrees,
|
||||
}: UseWorktreesOptions) {
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [worktrees, setWorktrees] = useState<WorktreeInfo[]>([]);
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
const currentWorktree = useAppStore((s) => s.getCurrentWorktree(projectPath));
|
||||
const setCurrentWorktree = useAppStore((s) => s.setCurrentWorktree);
|
||||
const setWorktreesInStore = useAppStore((s) => s.setWorktrees);
|
||||
const useWorktreesEnabled = useAppStore((s) => s.useWorktrees);
|
||||
|
||||
const fetchWorktrees = useCallback(
|
||||
async (options?: { silent?: boolean }) => {
|
||||
if (!projectPath) return;
|
||||
const silent = options?.silent ?? false;
|
||||
if (!silent) {
|
||||
setIsLoading(true);
|
||||
}
|
||||
try {
|
||||
const api = getElectronAPI();
|
||||
if (!api?.worktree?.listAll) {
|
||||
logger.warn('Worktree API not available');
|
||||
return;
|
||||
}
|
||||
const result = await api.worktree.listAll(projectPath, true);
|
||||
if (result.success && result.worktrees) {
|
||||
setWorktrees(result.worktrees);
|
||||
setWorktreesInStore(projectPath, result.worktrees);
|
||||
}
|
||||
// Return removed worktrees so they can be handled by the caller
|
||||
return result.removedWorktrees;
|
||||
} catch (error) {
|
||||
logger.error('Failed to fetch worktrees:', error);
|
||||
return undefined;
|
||||
} finally {
|
||||
if (!silent) {
|
||||
setIsLoading(false);
|
||||
}
|
||||
}
|
||||
},
|
||||
[projectPath, setWorktreesInStore]
|
||||
);
|
||||
// Use the React Query hook
|
||||
const { data, isLoading, refetch } = useWorktreesQuery(projectPath);
|
||||
const worktrees = (data?.worktrees ?? []) as WorktreeInfo[];
|
||||
|
||||
// Sync worktrees to Zustand store when they change
|
||||
useEffect(() => {
|
||||
fetchWorktrees();
|
||||
}, [fetchWorktrees]);
|
||||
if (worktrees.length > 0) {
|
||||
setWorktreesInStore(projectPath, worktrees);
|
||||
}
|
||||
}, [worktrees, projectPath, setWorktreesInStore]);
|
||||
|
||||
// Handle removed worktrees callback when data changes
|
||||
const prevRemovedWorktreesRef = useRef<string | null>(null);
|
||||
useEffect(() => {
|
||||
if (data?.removedWorktrees && data.removedWorktrees.length > 0) {
|
||||
// Create a stable key to avoid duplicate callbacks
|
||||
const key = JSON.stringify(data.removedWorktrees);
|
||||
if (key !== prevRemovedWorktreesRef.current) {
|
||||
prevRemovedWorktreesRef.current = key;
|
||||
onRemovedWorktrees?.(data.removedWorktrees);
|
||||
}
|
||||
}
|
||||
}, [data?.removedWorktrees, onRemovedWorktrees]);
|
||||
|
||||
// Handle refresh trigger
|
||||
useEffect(() => {
|
||||
if (refreshTrigger > 0) {
|
||||
fetchWorktrees().then((removedWorktrees) => {
|
||||
if (removedWorktrees && removedWorktrees.length > 0 && onRemovedWorktrees) {
|
||||
onRemovedWorktrees(removedWorktrees);
|
||||
}
|
||||
// Invalidate and refetch to get fresh data including any removed worktrees
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: queryKeys.worktrees.all(projectPath),
|
||||
});
|
||||
}
|
||||
}, [refreshTrigger, fetchWorktrees, onRemovedWorktrees]);
|
||||
}, [refreshTrigger, projectPath, queryClient]);
|
||||
|
||||
// Use a ref to track the current worktree to avoid running validation
|
||||
// when selection changes (which could cause a race condition with stale worktrees list)
|
||||
@@ -108,6 +94,14 @@ export function useWorktrees({
|
||||
[projectPath, setCurrentWorktree]
|
||||
);
|
||||
|
||||
// fetchWorktrees for backward compatibility - now just triggers a refetch
|
||||
const fetchWorktrees = useCallback(async () => {
|
||||
await queryClient.invalidateQueries({
|
||||
queryKey: queryKeys.worktrees.all(projectPath),
|
||||
});
|
||||
return refetch();
|
||||
}, [projectPath, queryClient, refetch]);
|
||||
|
||||
const currentWorktreePath = currentWorktree?.path ?? null;
|
||||
const selectedWorktree = currentWorktreePath
|
||||
? worktrees.find((w) => pathsEqual(w.path, currentWorktreePath))
|
||||
|
||||
@@ -5,6 +5,7 @@ import { cn, pathsEqual } from '@/lib/utils';
|
||||
import { toast } from 'sonner';
|
||||
import { getHttpApiClient } from '@/lib/http-api-client';
|
||||
import { useIsMobile } from '@/hooks/use-media-query';
|
||||
import { useWorktreeInitScript } from '@/hooks/queries';
|
||||
import type { WorktreePanelProps, WorktreeInfo } from './types';
|
||||
import {
|
||||
useWorktrees,
|
||||
@@ -79,42 +80,21 @@ export function WorktreePanel({
|
||||
handlePull,
|
||||
handlePush,
|
||||
handleOpenInEditor,
|
||||
} = useWorktreeActions({
|
||||
fetchWorktrees,
|
||||
fetchBranches,
|
||||
});
|
||||
} = useWorktreeActions();
|
||||
|
||||
const { hasRunningFeatures } = useRunningFeatures({
|
||||
runningFeatureIds,
|
||||
features,
|
||||
});
|
||||
|
||||
// Track whether init script exists for the project
|
||||
const [hasInitScript, setHasInitScript] = useState(false);
|
||||
// Check if init script exists for the project using React Query
|
||||
const { data: initScriptData } = useWorktreeInitScript(projectPath);
|
||||
const hasInitScript = initScriptData?.exists ?? false;
|
||||
|
||||
// Log panel state management
|
||||
const [logPanelOpen, setLogPanelOpen] = useState(false);
|
||||
const [logPanelWorktree, setLogPanelWorktree] = useState<WorktreeInfo | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (!projectPath) {
|
||||
setHasInitScript(false);
|
||||
return;
|
||||
}
|
||||
|
||||
const checkInitScript = async () => {
|
||||
try {
|
||||
const api = getHttpApiClient();
|
||||
const result = await api.worktree.getInitScript(projectPath);
|
||||
setHasInitScript(result.success && result.exists);
|
||||
} catch {
|
||||
setHasInitScript(false);
|
||||
}
|
||||
};
|
||||
|
||||
checkInitScript();
|
||||
}, [projectPath]);
|
||||
|
||||
const isMobile = useIsMobile();
|
||||
|
||||
// Periodic interval check (5 seconds) to detect branch changes on disk
|
||||
|
||||
Reference in New Issue
Block a user