diff --git a/src/main/AryxAppService.ts b/src/main/AryxAppService.ts index 8964494..5b7f237 100644 --- a/src/main/AryxAppService.ts +++ b/src/main/AryxAppService.ts @@ -473,27 +473,43 @@ export class AryxAppService extends EventEmitter { const selectedProject = selectedProjectId ? this.workspace.projects.find((project) => project.id === selectedProjectId) : undefined; - const didSyncUserTooling = await this.syncUserDiscoveredTooling(this.workspace); - const didSyncProjectTooling = selectedProject - ? await this.syncProjectDiscoveredTooling(this.workspace, selectedProject) - : false; - const didSyncProjectCustomization = selectedProject - ? await this.syncProjectCustomization(selectedProject) - : false; + + // Run independent sync operations in parallel + const [didSyncUserTooling, didSyncProjectTooling, didSyncProjectCustomization] = await Promise.all([ + this.syncUserDiscoveredTooling(this.workspace), + selectedProject + ? this.syncProjectDiscoveredTooling(this.workspace, selectedProject) + : false, + selectedProject + ? this.syncProjectCustomization(selectedProject) + : false, + ]); + const didPruneSelections = this.pruneUnavailableSessionToolingSelections(this.workspace); - const didPruneApprovalTools = await this.pruneUnavailableApprovalTools(this.workspace); if ( didSyncUserTooling || didSyncProjectTooling || didSyncProjectCustomization || didPruneSelections - || didPruneApprovalTools || this.cleanupInterruptedSessions(this.workspace) ) { await this.workspaceRepository.save(this.workspace); } await this.syncProjectCustomizationWatchers(this.workspace); + + // Defer sidecar-dependent approval pruning so it doesn't block startup. + // This runs in the background and emits workspace-updated when done. + void this.pruneUnavailableApprovalTools(this.workspace) + .then(async (didPrune) => { + if (didPrune && this.workspace) { + await this.workspaceRepository.save(this.workspace); + this.emit('workspace-updated', this.workspace); + } + }) + .catch((error) => { + console.error('[aryx startup] deferred approval tool pruning failed', error); + }); } if (!this.didScheduleInitialProjectGitRefresh) { diff --git a/src/main/index.ts b/src/main/index.ts index 6684a28..47a6758 100644 --- a/src/main/index.ts +++ b/src/main/index.ts @@ -23,21 +23,34 @@ async function bootstrap(): Promise { mainWindow = createMainWindow(); registerIpcHandlers(mainWindow, appService, autoUpdateService); - // Apply persisted theme to the title bar overlay - const workspace = await appService.loadWorkspace(); - applyTitleBarTheme(mainWindow, workspace.settings.theme); + // Start workspace loading in parallel — don't block window from showing. + // The renderer fetches the workspace via its own IPC call after mount. + const workspaceReady = appService.loadWorkspace(); - // Set up system tray - systemTray = new SystemTray({ - onShowWindow: showAndFocusWindow, - onCreateScratchpad: () => { - showAndFocusWindow(); - mainWindow?.webContents.send('tray:create-scratchpad'); - }, - onQuit: () => app.quit(), - }); - systemTray.create(); - systemTray.updateRunningCount(workspace); + // Apply theme and set up tray once workspace is available + workspaceReady + .then((workspace) => { + if (!mainWindow) return; + applyTitleBarTheme(mainWindow, workspace.settings.theme); + + systemTray = new SystemTray({ + onShowWindow: showAndFocusWindow, + onCreateScratchpad: () => { + showAndFocusWindow(); + mainWindow?.webContents.send('tray:create-scratchpad'); + }, + onQuit: () => app.quit(), + }); + systemTray.create(); + systemTray.updateRunningCount(workspace); + + appService!.on('workspace-updated', (updatedWorkspace) => { + systemTray?.updateRunningCount(updatedWorkspace); + }); + }) + .catch((error) => { + console.error('[aryx bootstrap] workspace load failed', error); + }); // Intercept close to hide to tray when the setting is enabled setupCloseToTray(mainWindow, () => { @@ -45,11 +58,6 @@ async function bootstrap(): Promise { return currentWorkspace?.settings.minimizeToTray === true; }); - // Keep tray status in sync when workspace changes - appService.on('workspace-updated', (updatedWorkspace) => { - systemTray?.updateRunningCount(updatedWorkspace); - }); - if (!app.isPackaged) { mainWindow.webContents.openDevTools({ mode: 'detach' }); } diff --git a/src/main/windows/createMainWindow.ts b/src/main/windows/createMainWindow.ts index 73fbaea..d229682 100644 --- a/src/main/windows/createMainWindow.ts +++ b/src/main/windows/createMainWindow.ts @@ -14,6 +14,7 @@ export function createMainWindow(): BrowserWindowType { height: 960, minWidth: 1120, minHeight: 720, + show: false, title: 'aryx', icon: resolveWindowIconPath({ appPath: app.getAppPath(), @@ -36,6 +37,10 @@ export function createMainWindow(): BrowserWindowType { }, }); + window.once('ready-to-show', () => { + window.show(); + }); + const rendererUrl = process.env.ELECTRON_RENDERER_URL; if (rendererUrl) { diff --git a/src/renderer/App.tsx b/src/renderer/App.tsx index 334f116..8c15a05 100644 --- a/src/renderer/App.tsx +++ b/src/renderer/App.tsx @@ -1,20 +1,7 @@ -import { useCallback, useEffect, useMemo, useRef, useState } from 'react'; +import { lazy, Suspense, useCallback, useEffect, useMemo, useRef, useState } from 'react'; -import { CommitComposer } from '@renderer/components/chat/CommitComposer'; import { AppShell } from '@renderer/components/AppShell'; -import { ActivityPanel } from '@renderer/components/ActivityPanel'; -import { ChatPane } from '@renderer/components/ChatPane'; -import { CommandPalette } from '@renderer/components/CommandPalette'; -import { DiscoveredToolingModal } from '@renderer/components/DiscoveredToolingModal'; -import { KeyboardShortcutsPanel } from '@renderer/components/KeyboardShortcutsPanel'; -import { ProjectSettingsPanel } from '@renderer/components/ProjectSettingsPanel'; -import { BookmarksPanel } from '@renderer/components/BookmarksPanel'; -import { SessionSearchPanel } from '@renderer/components/SessionSearchPanel'; -import { SettingsPanel, type SettingsSection } from '@renderer/components/SettingsPanel'; import { Sidebar } from '@renderer/components/Sidebar'; -import { BottomPanel, DEFAULT_HEIGHT as DEFAULT_BOTTOM_HEIGHT, MIN_HEIGHT as MIN_BOTTOM_HEIGHT, type BottomPanelTab } from '@renderer/components/BottomPanel'; -import { GitPanel } from '@renderer/components/GitPanel'; -import { TerminalPanel } from '@renderer/components/TerminalPanel'; import { resolveChatToolingSettings } from '@renderer/lib/chatTooling'; import { applySessionEventActivity, @@ -33,7 +20,6 @@ import { } from '@renderer/lib/sessionActivity'; import { applySubagentEvent, pruneSubagentMap, type ActiveSubagentMap } from '@renderer/lib/subagentTracker'; import { applySessionEventWorkspace } from '@renderer/lib/sessionWorkspace'; -import { WelcomePane } from '@renderer/components/WelcomePane'; import { getElectronApi } from '@renderer/lib/electronApi'; import { useTheme, useSidecarCapabilities } from '@renderer/hooks/useAppHooks'; import { @@ -52,7 +38,33 @@ import type { WorkspaceAgentDefinition } from '@shared/domain/workspaceAgent'; import type { WorkspaceState } from '@shared/domain/workspace'; import type { UpdateStatus } from '@shared/contracts/ipc'; import { createId, nowIso } from '@shared/utils/ids'; -import { WorkflowPicker } from '@renderer/components/workflow/WorkflowPicker'; + +// Lazy-loaded components — kept off the critical startup bundle. +// These pull in heavy dependencies (Lexical, @xyflow/react, @xterm/xterm, motion, etc.) +// that are not needed until the user interacts with the corresponding feature. +const ActivityPanel = lazy(() => import('@renderer/components/ActivityPanel').then((m) => ({ default: m.ActivityPanel }))); +const BookmarksPanel = lazy(() => import('@renderer/components/BookmarksPanel').then((m) => ({ default: m.BookmarksPanel }))); +const BottomPanel = lazy(() => import('@renderer/components/BottomPanel').then((m) => ({ default: m.BottomPanel }))); +const ChatPane = lazy(() => import('@renderer/components/ChatPane').then((m) => ({ default: m.ChatPane }))); +const CommandPalette = lazy(() => import('@renderer/components/CommandPalette').then((m) => ({ default: m.CommandPalette }))); +const CommitComposer = lazy(() => import('@renderer/components/chat/CommitComposer').then((m) => ({ default: m.CommitComposer }))); +const DiscoveredToolingModal = lazy(() => import('@renderer/components/DiscoveredToolingModal').then((m) => ({ default: m.DiscoveredToolingModal }))); +const GitPanel = lazy(() => import('@renderer/components/GitPanel').then((m) => ({ default: m.GitPanel }))); +const KeyboardShortcutsPanel = lazy(() => import('@renderer/components/KeyboardShortcutsPanel').then((m) => ({ default: m.KeyboardShortcutsPanel }))); +const ProjectSettingsPanel = lazy(() => import('@renderer/components/ProjectSettingsPanel').then((m) => ({ default: m.ProjectSettingsPanel }))); +const SessionSearchPanel = lazy(() => import('@renderer/components/SessionSearchPanel').then((m) => ({ default: m.SessionSearchPanel }))); +const SettingsPanel = lazy(() => import('@renderer/components/SettingsPanel').then((m) => ({ default: m.SettingsPanel }))); +const TerminalPanel = lazy(() => import('@renderer/components/TerminalPanel').then((m) => ({ default: m.TerminalPanel }))); +const WelcomePane = lazy(() => import('@renderer/components/WelcomePane').then((m) => ({ default: m.WelcomePane }))); +const WorkflowPicker = lazy(() => import('@renderer/components/workflow/WorkflowPicker').then((m) => ({ default: m.WorkflowPicker }))); + +// Re-export type-only imports from lazy modules so they're available without pulling in the bundle +type SettingsSection = 'appearance' | 'connection' | 'workflows' | 'agents' | 'mcp-servers' | 'lsp-profiles' | 'troubleshooting'; +type BottomPanelTab = 'terminal' | 'git'; + +// Constants duplicated from BottomPanel to avoid importing the full module at startup +const DEFAULT_BOTTOM_HEIGHT = 280; +const MIN_BOTTOM_HEIGHT = 120; function createDraftMcpServer(): McpServerDefinition { const timestamp = nowIso(); @@ -643,6 +655,9 @@ export default function App() { ); } + // Suspense fallback for lazy-loaded panels — intentionally blank to avoid layout flash + const lazyFallback = null; + // Determine main content let content: React.ReactNode; let detailPanel: React.ReactNode | undefined; @@ -657,6 +672,7 @@ export default function App() { ); } else if (selectedSession && workflowForSession && projectForSession) { content = ( + api.sendSessionMessage({ sessionId: selectedSession.id, @@ -740,30 +756,36 @@ export default function App() { onDiscardRunChanges={handleDiscardRunChanges} onOpenCommitComposer={handleOpenCommitComposer} /> + ); detailPanel = ( - + + + ); } else { content = ( - void api.addProject()} - onNewScratchpad={() => handleCreateScratchpad()} - onOpenSettings={() => setShowSettings(true)} - /> + + void api.addProject()} + onNewScratchpad={() => handleCreateScratchpad()} + onOpenSettings={() => setShowSettings(true)} + /> + ); } // Settings overlay const overlay = showSettings ? ( + api.getQuota()} /> + ) : null; return ( @@ -846,29 +869,31 @@ export default function App() { overlay={overlay} bottomPanel={ bottomPanelOpen ? ( - - ) : ( -
- Git is not available for scratchpad sessions -
- ) - } - gitDirty={gitDirty} - height={bottomPanelHeight} - onClose={handleBottomPanelClose} - onHeightChange={handleBottomPanelHeightChange} - onTabChange={setBottomPanelTab} - showGitTab={!!selectedSession && !isScratchpadProject(selectedSession.projectId)} - terminalContent={} - terminalRunning={terminalRunning} - /> + + + ) : ( +
+ Git is not available for scratchpad sessions +
+ ) + } + gitDirty={gitDirty} + height={bottomPanelHeight} + onClose={handleBottomPanelClose} + onHeightChange={handleBottomPanelHeightChange} + onTabChange={setBottomPanelTab} + showGitTab={!!selectedSession && !isScratchpadProject(selectedSession.projectId)} + terminalContent={} + terminalRunning={terminalRunning} + /> +
) : undefined } sidebar={ @@ -911,120 +936,136 @@ export default function App() { /> {showDiscoveryModal && ( - setShowDiscoveryModal(false)} - onResolveProjectServers={(serverIds, resolution) => { - if (selectedProject) { - void api.resolveProjectDiscoveredTooling({ projectId: selectedProject.id, serverIds, resolution }); - } - }} - onResolveUserServers={(serverIds, resolution) => { - void api.resolveWorkspaceDiscoveredTooling({ serverIds, resolution }); - }} - projectDiscoveredTooling={selectedProject?.discoveredTooling} - projectName={selectedProject?.name} - userDiscoveredTooling={workspace.settings.discoveredUserTooling} - /> + + setShowDiscoveryModal(false)} + onResolveProjectServers={(serverIds, resolution) => { + if (selectedProject) { + void api.resolveProjectDiscoveredTooling({ projectId: selectedProject.id, serverIds, resolution }); + } + }} + onResolveUserServers={(serverIds, resolution) => { + void api.resolveWorkspaceDiscoveredTooling({ serverIds, resolution }); + }} + projectDiscoveredTooling={selectedProject?.discoveredTooling} + projectName={selectedProject?.name} + userDiscoveredTooling={workspace.settings.discoveredUserTooling} + /> + )} {projectForSettings && ( - setProjectSettingsId(undefined)} - onRescanConfigs={() => { - void api.rescanProjectConfigs({ projectId: projectForSettings.id }); - }} - onRescanCustomization={() => { - void api.rescanProjectCustomization({ projectId: projectForSettings.id }); - }} - onResolveDiscoveredTooling={(serverIds, resolution) => { - void api.resolveProjectDiscoveredTooling({ projectId: projectForSettings.id, serverIds, resolution }); - }} - onSetAgentProfileEnabled={(agentProfileId, enabled) => { - void api.setProjectAgentProfileEnabled({ projectId: projectForSettings.id, agentProfileId, enabled }); - }} - onRemoveProject={() => { - void api.removeProject(projectForSettings.id); - setProjectSettingsId(undefined); - }} - /> + + setProjectSettingsId(undefined)} + onRescanConfigs={() => { + void api.rescanProjectConfigs({ projectId: projectForSettings.id }); + }} + onRescanCustomization={() => { + void api.rescanProjectCustomization({ projectId: projectForSettings.id }); + }} + onResolveDiscoveredTooling={(serverIds, resolution) => { + void api.resolveProjectDiscoveredTooling({ projectId: projectForSettings.id, serverIds, resolution }); + }} + onSetAgentProfileEnabled={(agentProfileId, enabled) => { + void api.setProjectAgentProfileEnabled({ projectId: projectForSettings.id, agentProfileId, enabled }); + }} + onRemoveProject={() => { + void api.removeProject(projectForSettings.id); + setProjectSettingsId(undefined); + }} + /> + )} {commandPaletteOpen && workspace && ( - setCommandPaletteOpen(false)} - onSelectSession={(sessionId) => { - void api.selectSession(sessionId); - }} - onSelectProject={(projectId) => { - void api.selectProject(projectId); - }} - onNewSession={(projectId) => handleNewSession(projectId)} - onCreateScratchpad={handleCreateScratchpad} - onOpenSettings={() => setShowSettings(true)} - onOpenProjectSettings={(projectId) => setProjectSettingsId(projectId)} - onToggleTerminal={handleTerminalToggle} - onSetTheme={(theme) => void api.setTheme(theme)} - onDuplicateSession={(sessionId) => { - void api.duplicateSession({ sessionId }); - }} - onPinSession={(sessionId, isPinned) => { - void api.setSessionPinned({ sessionId, isPinned }); - }} - onArchiveSession={(sessionId, isArchived) => { - void api.setSessionArchived({ sessionId, isArchived }); - }} - onAddProject={() => void api.addProject()} - onOpenAppDataFolder={() => void api.openAppDataFolder()} - onShowShortcuts={() => setShowShortcuts(true)} - onShowSearch={() => setShowSearch(true)} - onShowBookmarks={() => setShowBookmarks(true)} - /> + + setCommandPaletteOpen(false)} + onSelectSession={(sessionId) => { + void api.selectSession(sessionId); + }} + onSelectProject={(projectId) => { + void api.selectProject(projectId); + }} + onNewSession={(projectId) => handleNewSession(projectId)} + onCreateScratchpad={handleCreateScratchpad} + onOpenSettings={() => setShowSettings(true)} + onOpenProjectSettings={(projectId) => setProjectSettingsId(projectId)} + onToggleTerminal={handleTerminalToggle} + onSetTheme={(theme) => void api.setTheme(theme)} + onDuplicateSession={(sessionId) => { + void api.duplicateSession({ sessionId }); + }} + onPinSession={(sessionId, isPinned) => { + void api.setSessionPinned({ sessionId, isPinned }); + }} + onArchiveSession={(sessionId, isArchived) => { + void api.setSessionArchived({ sessionId, isArchived }); + }} + onAddProject={() => void api.addProject()} + onOpenAppDataFolder={() => void api.openAppDataFolder()} + onShowShortcuts={() => setShowShortcuts(true)} + onShowSearch={() => setShowSearch(true)} + onShowBookmarks={() => setShowBookmarks(true)} + /> + )} {showShortcuts && ( - setShowShortcuts(false)} /> + + setShowShortcuts(false)} /> + )} {showSearch && workspace && ( - setShowSearch(false)} - onSelectSession={(sessionId) => { - void api.selectSession(sessionId); - }} - /> + + setShowSearch(false)} + onSelectSession={(sessionId) => { + void api.selectSession(sessionId); + }} + /> + )} {showBookmarks && workspace && ( - setShowBookmarks(false)} - onSelectSession={(sessionId) => { - void api.selectSession(sessionId); - }} - onUnpinMessage={(sessionId, messageId) => { - void api.setSessionMessagePinned({ sessionId, messageId, isPinned: false }); - }} - /> + + setShowBookmarks(false)} + onSelectSession={(sessionId) => { + void api.selectSession(sessionId); + }} + onUnpinMessage={(sessionId, messageId) => { + void api.setSessionMessagePinned({ sessionId, messageId, isPinned: false }); + }} + /> + )} {commitComposerCtx && ( - setCommitComposerCtx(undefined)} - projectId={commitComposerCtx.projectId} - runId={commitComposerCtx.runId} - sessionId={commitComposerCtx.sessionId} - /> + + setCommitComposerCtx(undefined)} + projectId={commitComposerCtx.projectId} + runId={commitComposerCtx.runId} + sessionId={commitComposerCtx.sessionId} + /> + )} {workflowPickerProjectId && workspace && ( - setWorkflowPickerProjectId(null)} - /> + + setWorkflowPickerProjectId(null)} + /> + )} ); diff --git a/src/renderer/components/TerminalPanel.tsx b/src/renderer/components/TerminalPanel.tsx index b5747aa..838982b 100644 --- a/src/renderer/components/TerminalPanel.tsx +++ b/src/renderer/components/TerminalPanel.tsx @@ -3,6 +3,7 @@ import { RotateCcw } from 'lucide-react'; import { Terminal } from '@xterm/xterm'; import { FitAddon } from '@xterm/addon-fit'; import '@xterm/xterm/css/xterm.css'; +import '@fontsource-variable/jetbrains-mono'; import { getElectronApi } from '@renderer/lib/electronApi'; import type { TerminalSnapshot } from '@shared/domain/terminal'; diff --git a/src/renderer/main.tsx b/src/renderer/main.tsx index c4ca4ec..00dd85e 100644 --- a/src/renderer/main.tsx +++ b/src/renderer/main.tsx @@ -3,7 +3,6 @@ import { createRoot } from 'react-dom/client'; import '@fontsource-variable/outfit'; import '@fontsource-variable/dm-sans'; -import '@fontsource-variable/jetbrains-mono'; import App from '@renderer/App'; import '@renderer/styles.css';