feat: restore GLM 4.7 fixes - auto-scroll and retry logic
Changes from GLM 4.7 Progress Log: 1. Multi-task chat auto-scroll (multi-task-chat.tsx): - Added createEffect that monitors message count changes - Auto-scrolls using requestAnimationFrame + setTimeout(50ms) - Scrolls when new messages arrive or during streaming 2. Electron black screen fix (main.ts): - Added exponential backoff retry (1s, 2s, 4s, 8s, 16s max) - Added 30-second timeout for load operations - Added user-friendly error screen with retry button - Handles errno -3 network errors gracefully - Max 5 retry attempts before showing error
This commit is contained in:
@@ -18,6 +18,11 @@ let pendingCliUrl: string | null = null
|
|||||||
let showingLoadingScreen = false
|
let showingLoadingScreen = false
|
||||||
let preloadingView: BrowserView | null = null
|
let preloadingView: BrowserView | null = null
|
||||||
|
|
||||||
|
// Retry logic constants
|
||||||
|
const MAX_RETRY_ATTEMPTS = 5
|
||||||
|
const LOAD_TIMEOUT_MS = 30000
|
||||||
|
let retryAttempts = 0
|
||||||
|
|
||||||
if (isMac) {
|
if (isMac) {
|
||||||
app.commandLine.appendSwitch("disable-spell-checking")
|
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 = `
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<style>
|
||||||
|
body {
|
||||||
|
margin: 0;
|
||||||
|
padding: 40px;
|
||||||
|
font-family: system-ui, -apple-system, sans-serif;
|
||||||
|
background: #1a1a1a;
|
||||||
|
color: #fff;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
min-height: 100vh;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
.error-icon { font-size: 48px; margin-bottom: 20px; }
|
||||||
|
h1 { margin: 0 0 16px; font-size: 24px; font-weight: 600; }
|
||||||
|
p { margin: 0 0 24px; color: #888; font-size: 14px; text-align: center; max-width: 400px; }
|
||||||
|
.error-code { font-family: monospace; background: #2a2a2a; padding: 8px 16px; border-radius: 6px; font-size: 12px; color: #f87171; margin-bottom: 24px; }
|
||||||
|
button {
|
||||||
|
background: #6366f1;
|
||||||
|
color: white;
|
||||||
|
border: none;
|
||||||
|
padding: 12px 32px;
|
||||||
|
border-radius: 8px;
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 600;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.2s;
|
||||||
|
}
|
||||||
|
button:hover { background: #818cf8; transform: scale(1.02); }
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="error-icon">⚠️</div>
|
||||||
|
<h1>Connection Failed</h1>
|
||||||
|
<p>NomadArch couldn't connect to the development server after multiple attempts. Please ensure the server is running.</p>
|
||||||
|
<div class="error-code">${errorMessage}</div>
|
||||||
|
<button onclick="location.reload()">Retry</button>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
`
|
||||||
|
window.loadURL(`data:text/html;charset=utf-8,${encodeURIComponent(errorHtml)}`)
|
||||||
|
}
|
||||||
|
|
||||||
function getAllowedRendererOrigins(): string[] {
|
function getAllowedRendererOrigins(): string[] {
|
||||||
const origins = new Set<string>()
|
const origins = new Set<string>()
|
||||||
const rendererCandidates = [currentCliUrl, process.env.VITE_DEV_SERVER_URL, process.env.ELECTRON_RENDERER_URL]
|
const rendererCandidates = [currentCliUrl, process.env.VITE_DEV_SERVER_URL, process.env.ELECTRON_RENDERER_URL]
|
||||||
@@ -305,7 +365,55 @@ function finalizeCliSwap(url: string) {
|
|||||||
showingLoadingScreen = false
|
showingLoadingScreen = false
|
||||||
currentCliUrl = url
|
currentCliUrl = url
|
||||||
pendingCliUrl = null
|
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) => {
|
app.on("before-quit", async (event) => {
|
||||||
event.preventDefault()
|
event.preventDefault()
|
||||||
await cliManager.stop().catch(() => {})
|
await cliManager.stop().catch(() => { })
|
||||||
app.exit(0)
|
app.exit(0)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@@ -149,6 +149,19 @@ export default function MultiTaskChat(props: MultiTaskChatProps) {
|
|||||||
return () => clearInterval(interval);
|
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 handleSendMessage = async () => {
|
||||||
const message = chatInput().trim();
|
const message = chatInput().trim();
|
||||||
if (!message || isSending()) return;
|
if (!message || isSending()) return;
|
||||||
|
|||||||
Reference in New Issue
Block a user