diff --git a/packages/electron-app/electron/main/main.ts b/packages/electron-app/electron/main/main.ts index f4e8be1..14325bf 100644 --- a/packages/electron-app/electron/main/main.ts +++ b/packages/electron-app/electron/main/main.ts @@ -18,6 +18,11 @@ let pendingCliUrl: string | null = null let showingLoadingScreen = false let preloadingView: BrowserView | null = null +// Retry logic constants +const MAX_RETRY_ATTEMPTS = 5 +const LOAD_TIMEOUT_MS = 30000 +let retryAttempts = 0 + if (isMac) { app.commandLine.appendSwitch("disable-spell-checking") } @@ -89,6 +94,61 @@ function loadLoadingScreen(window: BrowserWindow) { }) } +// Calculate exponential backoff delay +function getRetryDelay(attempt: number): number { + return Math.min(1000 * Math.pow(2, attempt), 16000) // 1s, 2s, 4s, 8s, 16s max +} + +// Show user-friendly error screen +function showErrorScreen(window: BrowserWindow, errorMessage: string) { + const errorHtml = ` + + + + + + +
⚠️
+

Connection Failed

+

NomadArch couldn't connect to the development server after multiple attempts. Please ensure the server is running.

+
${errorMessage}
+ + + + ` + window.loadURL(`data:text/html;charset=utf-8,${encodeURIComponent(errorHtml)}`) +} + function getAllowedRendererOrigins(): string[] { const origins = new Set() const rendererCandidates = [currentCliUrl, process.env.VITE_DEV_SERVER_URL, process.env.ELECTRON_RENDERER_URL] @@ -305,7 +365,55 @@ function finalizeCliSwap(url: string) { showingLoadingScreen = false currentCliUrl = url pendingCliUrl = null - mainWindow.loadURL(url).catch((error) => console.error("[cli] failed to load CLI view:", error)) + + // Reset retry counter on new URL + retryAttempts = 0 + + const loadWithRetry = () => { + if (!mainWindow || mainWindow.isDestroyed()) return + + // Set timeout for load + const timeoutId = setTimeout(() => { + console.warn(`[cli] Load timeout after ${LOAD_TIMEOUT_MS}ms`) + handleLoadError(new Error(`Load timeout after ${LOAD_TIMEOUT_MS}ms`)) + }, LOAD_TIMEOUT_MS) + + mainWindow.loadURL(url) + .then(() => { + clearTimeout(timeoutId) + retryAttempts = 0 // Reset on success + console.info("[cli] Successfully loaded CLI view") + }) + .catch((error) => { + clearTimeout(timeoutId) + handleLoadError(error) + }) + } + + const handleLoadError = (error: Error) => { + const errorCode = (error as any).errno + console.error(`[cli] failed to load CLI view (attempt ${retryAttempts + 1}/${MAX_RETRY_ATTEMPTS}):`, error.message) + + // Retry on network errors (errno -3) + if (errorCode === -3 && retryAttempts < MAX_RETRY_ATTEMPTS) { + retryAttempts++ + const delay = getRetryDelay(retryAttempts) + console.info(`[cli] Retrying in ${delay}ms (attempt ${retryAttempts}/${MAX_RETRY_ATTEMPTS})`) + + if (mainWindow && !mainWindow.isDestroyed()) { + loadLoadingScreen(mainWindow) + } + + setTimeout(loadWithRetry, delay) + } else if (retryAttempts >= MAX_RETRY_ATTEMPTS) { + console.error("[cli] Max retry attempts reached, showing error screen") + if (mainWindow && !mainWindow.isDestroyed()) { + showErrorScreen(mainWindow, `Failed after ${MAX_RETRY_ATTEMPTS} attempts: ${error.message}`) + } + } + } + + loadWithRetry() } @@ -370,7 +478,7 @@ app.whenReady().then(() => { app.on("before-quit", async (event) => { event.preventDefault() - await cliManager.stop().catch(() => {}) + await cliManager.stop().catch(() => { }) app.exit(0) }) diff --git a/packages/ui/src/components/chat/multi-task-chat.tsx b/packages/ui/src/components/chat/multi-task-chat.tsx index 1c78fc1..0a69a42 100644 --- a/packages/ui/src/components/chat/multi-task-chat.tsx +++ b/packages/ui/src/components/chat/multi-task-chat.tsx @@ -149,6 +149,19 @@ export default function MultiTaskChat(props: MultiTaskChatProps) { return () => clearInterval(interval); }); + // Auto-scroll when new messages arrive + createEffect(() => { + const ids = filteredMessageIds(); + const thinking = isAgentThinking(); + + // Scroll when message count changes or when thinking starts + if (ids.length > 0 || thinking) { + requestAnimationFrame(() => { + setTimeout(scrollToBottom, 50); + }); + } + }); + const handleSendMessage = async () => { const message = chatInput().trim(); if (!message || isSending()) return;