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;