From ceee391a8249e70f8949c1306846b2ef8deedad5 Mon Sep 17 00:00:00 2001 From: gsxdsm Date: Wed, 4 Mar 2026 09:29:00 -0800 Subject: [PATCH 1/2] Changes from fix/feature-deeplink-worktree --- .../server/src/services/event-hook-service.ts | 6 +- .../components/notification-bell.tsx | 8 +- apps/ui/src/components/views/board-view.tsx | 135 ++++++++++++-- .../kanban-card/agent-info-panel.tsx | 164 ++++++++++++++++-- .../components/views/notifications-view.tsx | 10 +- .../views/overview/recent-activity-feed.tsx | 10 +- apps/ui/src/routes/board.lazy.tsx | 4 +- apps/ui/src/routes/board.tsx | 1 + 8 files changed, 297 insertions(+), 41 deletions(-) diff --git a/apps/server/src/services/event-hook-service.ts b/apps/server/src/services/event-hook-service.ts index 005e7e547..816a6ed27 100644 --- a/apps/server/src/services/event-hook-service.ts +++ b/apps/server/src/services/event-hook-service.ts @@ -595,12 +595,12 @@ export class EventHookService { if (clickUrl && context.projectPath) { try { const url = new URL(clickUrl); + url.pathname = '/board'; + // Add projectPath so the UI can switch to the correct project + url.searchParams.set('projectPath', context.projectPath); // Add featureId as query param for deep linking to board with feature output modal if (context.featureId) { - url.pathname = '/board'; url.searchParams.set('featureId', context.featureId); - } else { - url.pathname = '/board'; } clickUrl = url.toString(); } catch (error) { diff --git a/apps/ui/src/components/layout/project-switcher/components/notification-bell.tsx b/apps/ui/src/components/layout/project-switcher/components/notification-bell.tsx index 8145f971e..747a9010b 100644 --- a/apps/ui/src/components/layout/project-switcher/components/notification-bell.tsx +++ b/apps/ui/src/components/layout/project-switcher/components/notification-bell.tsx @@ -68,7 +68,13 @@ export function NotificationBell({ projectPath }: NotificationBellProps) { // Navigate to the relevant view based on notification type if (notification.featureId) { - navigate({ to: '/board', search: { featureId: notification.featureId } }); + navigate({ + to: '/board', + search: { + featureId: notification.featureId, + projectPath: notification.projectPath || undefined, + }, + }); } }, [handleMarkAsRead, setPopoverOpen, navigate] diff --git a/apps/ui/src/components/views/board-view.tsx b/apps/ui/src/components/views/board-view.tsx index ecbbe8a7d..97c5b44cb 100644 --- a/apps/ui/src/components/views/board-view.tsx +++ b/apps/ui/src/components/views/board-view.tsx @@ -1,4 +1,4 @@ -import { useEffect, useState, useCallback, useMemo, useRef } from 'react'; +import { useEffect, useState, useCallback, useMemo, useRef, startTransition } from 'react'; import { createLogger } from '@automaker/utils/logger'; import type { PointerEvent as ReactPointerEvent } from 'react'; import { @@ -37,6 +37,7 @@ import type { ReasoningEffort, } from '@automaker/types'; import { pathsEqual } from '@/lib/utils'; +import { initializeProject } from '@/lib/project-init'; import { toast } from 'sonner'; import { BoardBackgroundModal, @@ -117,9 +118,11 @@ const logger = createLogger('Board'); interface BoardViewProps { /** Feature ID from URL parameter - if provided, opens output modal for this feature on load */ initialFeatureId?: string; + /** Project path from URL parameter - if provided, switches to this project before handling deep link */ + initialProjectPath?: string; } -export function BoardView({ initialFeatureId }: BoardViewProps) { +export function BoardView({ initialFeatureId, initialProjectPath }: BoardViewProps) { const { currentProject, defaultSkipTests, @@ -139,6 +142,7 @@ export function BoardView({ initialFeatureId }: BoardViewProps) { setPipelineConfig, featureTemplates, defaultSortNewestCardOnTop, + upsertAndSetCurrentProject, } = useAppStore( useShallow((state) => ({ currentProject: state.currentProject, @@ -159,6 +163,7 @@ export function BoardView({ initialFeatureId }: BoardViewProps) { setPipelineConfig: state.setPipelineConfig, featureTemplates: state.featureTemplates, defaultSortNewestCardOnTop: state.defaultSortNewestCardOnTop, + upsertAndSetCurrentProject: state.upsertAndSetCurrentProject, })) ); // Also get keyboard shortcuts for the add feature shortcut @@ -305,6 +310,53 @@ export function BoardView({ initialFeatureId }: BoardViewProps) { setFeaturesWithContext, }); + // Handle deep link project switching - if URL includes a projectPath that differs from + // the current project, switch to the target project first. The feature/worktree deep link + // effect below will fire naturally once the project switch triggers a features reload. + const handledProjectPathRef = useRef(undefined); + useEffect(() => { + if (!initialProjectPath || handledProjectPathRef.current === initialProjectPath) { + return; + } + + // Check if we're already on the correct project + if (currentProject?.path && pathsEqual(currentProject.path, initialProjectPath)) { + handledProjectPathRef.current = initialProjectPath; + return; + } + + handledProjectPathRef.current = initialProjectPath; + + const switchProject = async () => { + try { + const initResult = await initializeProject(initialProjectPath); + if (!initResult.success) { + logger.warn( + `Deep link: failed to initialize project "${initialProjectPath}":`, + initResult.error + ); + toast.error('Failed to open project from link', { + description: initResult.error || 'Unknown error', + }); + return; + } + + // Derive project name from path basename + const projectName = + initialProjectPath.split('/').filter(Boolean).pop() || initialProjectPath; + logger.info(`Deep link: switching to project "${projectName}" at ${initialProjectPath}`); + upsertAndSetCurrentProject(initialProjectPath, projectName); + } catch (error) { + logger.error('Deep link: project switch failed:', error); + toast.error('Failed to switch project', { + description: error instanceof Error ? error.message : 'Unknown error', + }); + } + }; + + switchProject(); + }, [initialProjectPath, currentProject?.path, upsertAndSetCurrentProject]); + // Handle initial feature ID from URL - switch to the correct worktree and open output modal // Uses a ref to track which featureId has been handled to prevent re-opening // when the component re-renders but initialFeatureId hasn't changed. @@ -325,6 +377,17 @@ export function BoardView({ initialFeatureId }: BoardViewProps) { [currentProject?.path] ) ); + + // Track how many render cycles we've waited for worktrees during a deep link. + // If the Zustand store never gets populated (e.g., WorktreePanel hasn't mounted, + // useWorktrees setting is off, or the worktree query failed), we stop waiting + // after a threshold and open the modal without switching worktree. + const deepLinkRetryCountRef = useRef(0); + // Reset retry count when the feature ID changes + useEffect(() => { + deepLinkRetryCountRef.current = 0; + }, [initialFeatureId]); + useEffect(() => { if ( !initialFeatureId || @@ -339,14 +402,43 @@ export function BoardView({ initialFeatureId }: BoardViewProps) { const feature = hookFeatures.find((f) => f.id === initialFeatureId); if (!feature) return; - // If the feature has a branch, wait for worktrees to load so we can switch - if (feature.branchName && deepLinkWorktrees.length === 0) { - return; // Worktrees not loaded yet - effect will re-run when they load + // Resolve worktrees: prefer the Zustand store (reactive), but fall back to + // the React Query cache if the store hasn't been populated yet. The store is + // only synced by the WorktreePanel's useWorktrees hook, which may not have + // rendered yet during a deep link cold start. Reading the query cache directly + // avoids an indefinite wait that hangs the app on the loading screen. + let resolvedWorktrees = deepLinkWorktrees; + if (resolvedWorktrees.length === 0 && currentProject.path) { + const cachedData = queryClient.getQueryData(queryKeys.worktrees.all(currentProject.path)) as + | { worktrees?: WorktreeInfo[] } + | undefined; + if (cachedData?.worktrees && cachedData.worktrees.length > 0) { + resolvedWorktrees = cachedData.worktrees as typeof deepLinkWorktrees; + } + } + + // If the feature has a branch and worktrees aren't available yet, wait briefly. + // After enough retries, proceed without switching worktree to avoid hanging. + const MAX_DEEP_LINK_RETRIES = 10; + if (feature.branchName && resolvedWorktrees.length === 0) { + deepLinkRetryCountRef.current++; + if (deepLinkRetryCountRef.current < MAX_DEEP_LINK_RETRIES) { + return; // Worktrees not loaded yet - effect will re-run when they load + } + // Exceeded retry limit — proceed without worktree switch to avoid hanging + logger.warn( + `Deep link: worktrees not available after ${MAX_DEEP_LINK_RETRIES} retries, ` + + `opening feature ${initialFeatureId} without switching worktree` + ); } - // Switch to the correct worktree based on the feature's branchName - if (feature.branchName && deepLinkWorktrees.length > 0) { - const targetWorktree = deepLinkWorktrees.find((w) => w.branch === feature.branchName); + // Switch to the correct worktree based on the feature's branchName. + // IMPORTANT: Wrap in startTransition to batch the Zustand store update with + // any concurrent React state updates. Without this, the synchronous store + // mutation cascades through useAutoMode → refreshStatus → setAutoModeRunning, + // which can trigger React error #185 on mobile Safari/PWA crash loops. + if (feature.branchName && resolvedWorktrees.length > 0) { + const targetWorktree = resolvedWorktrees.find((w) => w.branch === feature.branchName); if (targetWorktree) { const currentWt = useAppStore.getState().getCurrentWorktree(currentProject.path); const isAlreadySelected = targetWorktree.isMain @@ -356,23 +448,27 @@ export function BoardView({ initialFeatureId }: BoardViewProps) { logger.info( `Deep link: switching to worktree "${targetWorktree.branch}" for feature ${initialFeatureId}` ); - setCurrentWorktree( - currentProject.path, - targetWorktree.isMain ? null : targetWorktree.path, - targetWorktree.branch - ); + startTransition(() => { + setCurrentWorktree( + currentProject.path, + targetWorktree.isMain ? null : targetWorktree.path, + targetWorktree.branch + ); + }); } } - } else if (!feature.branchName && deepLinkWorktrees.length > 0) { + } else if (!feature.branchName && resolvedWorktrees.length > 0) { // Feature has no branch - should be on the main worktree const currentWt = useAppStore.getState().getCurrentWorktree(currentProject.path); if (currentWt?.path !== null && currentWt !== null) { - const mainWorktree = deepLinkWorktrees.find((w) => w.isMain); + const mainWorktree = resolvedWorktrees.find((w) => w.isMain); if (mainWorktree) { logger.info( `Deep link: switching to main worktree for unassigned feature ${initialFeatureId}` ); - setCurrentWorktree(currentProject.path, null, mainWorktree.branch); + startTransition(() => { + setCurrentWorktree(currentProject.path, null, mainWorktree.branch); + }); } } } @@ -387,6 +483,7 @@ export function BoardView({ initialFeatureId }: BoardViewProps) { hookFeatures, currentProject?.path, deepLinkWorktrees, + queryClient, setCurrentWorktree, setOutputFeature, setShowOutputModal, @@ -764,11 +861,15 @@ export function BoardView({ initialFeatureId }: BoardViewProps) { // Recovery handler for BoardErrorBoundary: reset worktree selection to main // so the board can re-render without the stale worktree state that caused the crash. + // Wrapped in startTransition to batch with concurrent React updates and avoid + // triggering another cascade during recovery. const handleBoardRecover = useCallback(() => { if (!currentProject) return; const mainWorktree = worktrees.find((w) => w.isMain); const mainBranch = mainWorktree?.branch || 'main'; - setCurrentWorktree(currentProject.path, null, mainBranch); + startTransition(() => { + setCurrentWorktree(currentProject.path, null, mainBranch); + }); }, [currentProject, worktrees, setCurrentWorktree]); // Helper function to add and select a worktree diff --git a/apps/ui/src/components/views/board-view/components/kanban-card/agent-info-panel.tsx b/apps/ui/src/components/views/board-view/components/kanban-card/agent-info-panel.tsx index fa6f3b0c0..551fcb6ad 100644 --- a/apps/ui/src/components/views/board-view/components/kanban-card/agent-info-panel.tsx +++ b/apps/ui/src/components/views/board-view/components/kanban-card/agent-info-panel.tsx @@ -14,6 +14,63 @@ import { useFeature, useAgentOutput } from '@/hooks/queries'; import { queryKeys } from '@/lib/query-keys'; import { getFirstNonEmptySummary } from '@/lib/summary-selection'; import { useAppStore } from '@/store/app-store'; +import { isMobileDevice } from '@/lib/mobile-detect'; + +// Global concurrency control for mobile mount staggering. +// When many AgentInfoPanel instances mount simultaneously (e.g., worktree switch +// with 50+ cards), we spread queries over a wider window and cap how many +// panels can be querying concurrently to prevent mobile Safari crashes. +// +// The mechanism works in two layers: +// 1. Random delay (0-6s) - spreads mount times so not all panels try to query at once +// 2. Concurrency slots (max 4) - even after the delay, only N panels can query simultaneously +// +// Instance tracking ensures the queue resets if all panels unmount (e.g., navigation). +const MOBILE_MAX_CONCURRENT_QUERIES = 4; +const MOBILE_STAGGER_WINDOW_MS = 6000; // 6s window (vs previous 2s) +let activeMobileQueryCount = 0; +let pendingMobileQueue: Array<() => void> = []; +let mountedPanelCount = 0; + +function acquireMobileQuerySlot(): Promise { + if (!isMobileDevice) return Promise.resolve(); + if (activeMobileQueryCount < MOBILE_MAX_CONCURRENT_QUERIES) { + activeMobileQueryCount++; + return Promise.resolve(); + } + return new Promise((resolve) => { + pendingMobileQueue.push(() => { + activeMobileQueryCount++; + resolve(); + }); + }); +} + +function releaseMobileQuerySlot(): void { + if (!isMobileDevice) return; + activeMobileQueryCount = Math.max(0, activeMobileQueryCount - 1); + const next = pendingMobileQueue.shift(); + if (next) next(); +} + +function trackPanelMount(): void { + if (!isMobileDevice) return; + mountedPanelCount++; +} + +function trackPanelUnmount(): void { + if (!isMobileDevice) return; + mountedPanelCount = Math.max(0, mountedPanelCount - 1); + // If all panels unmounted (e.g., navigated away from board or worktree switch), + // reset the queue to prevent stale state from blocking future mounts. + if (mountedPanelCount === 0) { + activeMobileQueryCount = 0; + // Drain any pending callbacks so their Promises resolve (components already unmounted) + const pending = pendingMobileQueue; + pendingMobileQueue = []; + for (const cb of pending) cb(); + } +} /** * Formats thinking level for compact display @@ -66,6 +123,12 @@ export const AgentInfoPanel = memo(function AgentInfoPanel({ const [isSummaryDialogOpen, setIsSummaryDialogOpen] = useState(false); const [isTodosExpanded, setIsTodosExpanded] = useState(false); + // Track mounted panel count for global queue reset on full unmount + useEffect(() => { + trackPanelMount(); + return () => trackPanelUnmount(); + }, []); + // Get providers from store for provider-aware model name display // This allows formatModelName to show provider-specific model names (e.g., "GLM 4.7" instead of "Sonnet 4.5") // when a feature was executed using a Claude-compatible provider @@ -92,6 +155,41 @@ export const AgentInfoPanel = memo(function AgentInfoPanel({ // Determine if we should poll for updates const shouldFetchData = feature.status !== 'backlog' && feature.status !== 'merge_conflict'; + // On mobile, stagger initial per-card queries to prevent a mount storm. + // When a worktree loads with many cards, all AgentInfoPanel instances mount + // simultaneously. Without staggering, each card fires useFeature + useAgentOutput + // queries at the same time, creating 60-100+ concurrent API calls that crash + // mobile Safari. Actively running cards fetch immediately (priority data); + // other cards defer by a random delay AND wait for a concurrency slot. + // The stagger window is 6s (vs previous 2s) to spread load for worktrees + // with 50+ features. The concurrency limiter caps active queries to 4 at a time, + // preventing the burst that overwhelms mobile Safari's connection handling. + const [mountReady, setMountReady] = useState(!isMobileDevice || !!isActivelyRunning); + useEffect(() => { + if (mountReady) return; + let cancelled = false; + const delay = Math.random() * MOBILE_STAGGER_WINDOW_MS; + const timer = setTimeout(() => { + // After the random delay, also wait for a concurrency slot + acquireMobileQuerySlot().then(() => { + if (!cancelled) { + setMountReady(true); + // Release the slot after a brief window to let the initial queries fire + // and return, preventing all slots from being held indefinitely + setTimeout(releaseMobileQuerySlot, 3000); + } else { + releaseMobileQuerySlot(); + } + }); + }, delay); + return () => { + cancelled = true; + clearTimeout(timer); + }; + }, [mountReady]); + + const queryEnabled = shouldFetchData && mountReady; + // Track whether we're receiving WebSocket events (within threshold) // Use a state to trigger re-renders when the WebSocket connection becomes stale const [isReceivingWsEvents, setIsReceivingWsEvents] = useState(false); @@ -142,34 +240,72 @@ export const AgentInfoPanel = memo(function AgentInfoPanel({ // Fetch fresh feature data for planSpec (store data can be stale for task progress) const { data: freshFeature } = useFeature(projectPath, feature.id, { - enabled: shouldFetchData && !contextContent, + enabled: queryEnabled && !contextContent, pollingInterval, }); // Fetch agent output for parsing const { data: agentOutputContent } = useAgentOutput(projectPath, feature.id, { - enabled: shouldFetchData && !contextContent, + enabled: queryEnabled && !contextContent, pollingInterval, }); // On mount, ensure feature and agent output queries are fresh. // This handles the worktree switch scenario where cards unmount when filtered out // and remount when the user switches back. Without this, the React Query cache - // may serve stale data (or no data) for the individual feature query, causing - // the todo list to appear empty until the next polling cycle. + // may serve stale data for the individual feature query, causing the todo list + // to appear empty until the next polling cycle. + // + // IMPORTANT: Only invalidate if the cached data EXISTS and is STALE. + // During worktree switches, ALL cards in the new worktree remount simultaneously. + // If every card fires invalidateQueries(), it creates a query storm (40-100+ + // concurrent invalidations) that overwhelms React's rendering pipeline on mobile + // Safari/PWA, causing crashes. The key insight: if a query has NEVER been fetched + // (no dataUpdatedAt), there's nothing stale to invalidate — the useFeature/ + // useAgentOutput hooks will fetch fresh data when their `enabled` flag is true. + // We only need to invalidate when cached data exists but is outdated. + // + // On mobile, skip mount-time invalidation entirely. The staggered useFeature/ + // useAgentOutput queries already fetch fresh data — invalidation is redundant + // and creates the exact query storm we're trying to prevent. The stale threshold + // is also higher on mobile (30s vs 10s) to further reduce unnecessary refetches + // during the settling period after a worktree switch. useEffect(() => { - if (shouldFetchData && projectPath && feature.id && !contextContent) { - // Invalidate both the single feature and agent output queries to trigger immediate refetch - queryClient.invalidateQueries({ - queryKey: queryKeys.features.single(projectPath, feature.id), - }); - queryClient.invalidateQueries({ - queryKey: queryKeys.features.agentOutput(projectPath, feature.id), - }); + if (queryEnabled && projectPath && feature.id && !contextContent) { + // On mobile, skip mount-time invalidation — the useFeature/useAgentOutput + // hooks will handle the initial fetch after the stagger delay. + if (isMobileDevice) return; + + const MOUNT_STALE_THRESHOLD = 10_000; // 10s — skip invalidation if data is fresh + const now = Date.now(); + + const featureQuery = queryClient.getQueryState( + queryKeys.features.single(projectPath, feature.id) + ); + const agentOutputQuery = queryClient.getQueryState( + queryKeys.features.agentOutput(projectPath, feature.id) + ); + + // Only invalidate queries that have cached data AND are stale. + // Skip if the query has never been fetched (dataUpdatedAt is undefined) — + // the useFeature/useAgentOutput hooks will handle the initial fetch. + if (featureQuery?.dataUpdatedAt && now - featureQuery.dataUpdatedAt > MOUNT_STALE_THRESHOLD) { + queryClient.invalidateQueries({ + queryKey: queryKeys.features.single(projectPath, feature.id), + }); + } + if ( + agentOutputQuery?.dataUpdatedAt && + now - agentOutputQuery.dataUpdatedAt > MOUNT_STALE_THRESHOLD + ) { + queryClient.invalidateQueries({ + queryKey: queryKeys.features.agentOutput(projectPath, feature.id), + }); + } } - // Only run on mount (feature.id and projectPath identify this specific card instance) + // Runs when mount staggering completes (queryEnabled becomes true) or on initial mount // eslint-disable-next-line react-hooks/exhaustive-deps - }, [feature.id, projectPath]); + }, [queryEnabled, feature.id, projectPath]); // Parse agent output into agentInfo const agentInfo = useMemo(() => { diff --git a/apps/ui/src/components/views/notifications-view.tsx b/apps/ui/src/components/views/notifications-view.tsx index 112372f75..a65c91e9b 100644 --- a/apps/ui/src/components/views/notifications-view.tsx +++ b/apps/ui/src/components/views/notifications-view.tsx @@ -94,8 +94,14 @@ export function NotificationsView() { // Navigate to the relevant view based on notification type if (notification.featureId) { - // Navigate to board view with feature ID to show output - navigate({ to: '/board', search: { featureId: notification.featureId } }); + // Navigate to board view with feature ID and project path to show output + navigate({ + to: '/board', + search: { + featureId: notification.featureId, + projectPath: notification.projectPath || undefined, + }, + }); } }, [handleMarkAsRead, navigate] diff --git a/apps/ui/src/components/views/overview/recent-activity-feed.tsx b/apps/ui/src/components/views/overview/recent-activity-feed.tsx index 83ec5ebc9..321be9e5a 100644 --- a/apps/ui/src/components/views/overview/recent-activity-feed.tsx +++ b/apps/ui/src/components/views/overview/recent-activity-feed.tsx @@ -136,8 +136,14 @@ export function RecentActivityFeed({ activities, maxItems = 10 }: RecentActivity upsertAndSetCurrentProject(projectPath, projectName); if (activity.featureId) { - // Navigate to the specific feature - navigate({ to: '/board', search: { featureId: activity.featureId } }); + // Navigate to the specific feature with project path for deep link handling + navigate({ + to: '/board', + search: { + featureId: activity.featureId, + projectPath: projectPath || undefined, + }, + }); } else { navigate({ to: '/board' }); } diff --git a/apps/ui/src/routes/board.lazy.tsx b/apps/ui/src/routes/board.lazy.tsx index acae2330c..d9a0835a2 100644 --- a/apps/ui/src/routes/board.lazy.tsx +++ b/apps/ui/src/routes/board.lazy.tsx @@ -6,6 +6,6 @@ export const Route = createLazyFileRoute('/board')({ }); function BoardRouteComponent() { - const { featureId } = useSearch({ from: '/board' }); - return ; + const { featureId, projectPath } = useSearch({ from: '/board' }); + return ; } diff --git a/apps/ui/src/routes/board.tsx b/apps/ui/src/routes/board.tsx index 689ce62b6..1f029d5ec 100644 --- a/apps/ui/src/routes/board.tsx +++ b/apps/ui/src/routes/board.tsx @@ -4,6 +4,7 @@ import { z } from 'zod'; // Search params schema for board route const boardSearchSchema = z.object({ featureId: z.string().optional(), + projectPath: z.string().optional(), }); // Component is lazy-loaded via board.lazy.tsx for code splitting. From 8345c9159cefeae95762de927e0549dbaf0b0800 Mon Sep 17 00:00:00 2001 From: gsxdsm Date: Wed, 4 Mar 2026 10:08:39 -0800 Subject: [PATCH 2/2] Update apps/ui/src/components/views/board-view.tsx Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com> --- apps/ui/src/components/views/board-view.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/ui/src/components/views/board-view.tsx b/apps/ui/src/components/views/board-view.tsx index 97c5b44cb..8268ca411 100644 --- a/apps/ui/src/components/views/board-view.tsx +++ b/apps/ui/src/components/views/board-view.tsx @@ -343,7 +343,7 @@ export function BoardView({ initialFeatureId, initialProjectPath }: BoardViewPro // Derive project name from path basename const projectName = - initialProjectPath.split('/').filter(Boolean).pop() || initialProjectPath; + initialProjectPath.split(/[/\\]/).filter(Boolean).pop() || initialProjectPath; logger.info(`Deep link: switching to project "${projectName}" at ${initialProjectPath}`); upsertAndSetCurrentProject(initialProjectPath, projectName); } catch (error) {