diff --git a/electron/main.ts b/electron/main.ts index 9afcbd0..c4f2568 100644 --- a/electron/main.ts +++ b/electron/main.ts @@ -361,6 +361,7 @@ function getPort(): Promise { async function waitForServer(port: number, timeout = 30000): Promise { const start = Date.now(); + let lastError = ''; while (Date.now() - start < timeout) { // If the server process already exited, fail fast if (serverExited) { @@ -371,23 +372,33 @@ async function waitForServer(port: number, timeout = 30000): Promise { try { await new Promise((resolve, reject) => { // eslint-disable-next-line @typescript-eslint/no-require-imports - const req = require('http').get(`http://127.0.0.1:${port}/api/health`, (res: { statusCode?: number }) => { + const http = require('http'); + // Use options object with family:4 to force IPv4 — avoids Windows + // IPv6 resolution issues where 127.0.0.1 may fail to connect. + const req = http.get({ + hostname: '127.0.0.1', + port, + path: '/api/health', + family: 4, + timeout: 2000, + }, (res: { statusCode?: number }) => { if (res.statusCode === 200) resolve(); else reject(new Error(`Status ${res.statusCode}`)); }); - req.on('error', reject); - req.setTimeout(1000, () => { + req.on('error', (err: Error) => reject(err)); + req.on('timeout', () => { req.destroy(); - reject(new Error('timeout')); + reject(new Error('request timeout')); }); }); return; - } catch { - await new Promise(r => setTimeout(r, 200)); + } catch (err) { + lastError = err instanceof Error ? err.message : String(err); + await new Promise(r => setTimeout(r, 300)); } } throw new Error( - `Server startup timeout after ${timeout / 1000}s.\n\n${serverErrors.length > 0 ? 'Server output:\n' + serverErrors.slice(-10).join('\n') : 'No server output captured.'}` + `Server startup timeout after ${timeout / 1000}s.\n\nLast health-check error: ${lastError}\n\n${serverErrors.length > 0 ? 'Server output:\n' + serverErrors.slice(-10).join('\n') : 'No server output captured.'}` ); } diff --git a/src/app/chat/page.tsx b/src/app/chat/page.tsx index e8bf83b..f78e46b 100644 --- a/src/app/chat/page.tsx +++ b/src/app/chat/page.tsx @@ -42,15 +42,16 @@ export default function NewChatPage() { abortControllerRef.current = null; }, []); - const handlePermissionResponse = useCallback(async (decision: 'allow' | 'allow_session' | 'deny') => { + const handlePermissionResponse = useCallback(async (decision: 'allow' | 'allow_session' | 'deny', updatedInput?: Record, denyMessage?: string) => { if (!pendingPermission) return; - const body: { permissionRequestId: string; decision: { behavior: 'allow'; updatedPermissions?: unknown[] } | { behavior: 'deny'; message?: string } } = { + const body: { permissionRequestId: string; decision: { behavior: 'allow'; updatedInput?: Record; updatedPermissions?: unknown[] } | { behavior: 'deny'; message?: string } } = { permissionRequestId: pendingPermission.permissionRequestId, decision: decision === 'deny' - ? { behavior: 'deny', message: 'User denied permission' } + ? { behavior: 'deny', message: denyMessage || 'User denied permission' } : { behavior: 'allow', + ...(updatedInput ? { updatedInput } : {}), ...(decision === 'allow_session' && pendingPermission.suggestions ? { updatedPermissions: pendingPermission.suggestions } : {}), diff --git a/src/components/chat/ChatView.tsx b/src/components/chat/ChatView.tsx index 6daef5f..6b4d0d7 100644 --- a/src/components/chat/ChatView.tsx +++ b/src/components/chat/ChatView.tsx @@ -118,18 +118,30 @@ export function ChatView({ sessionId, initialMessages = [], initialHasMore = fal if (existing.pendingPermission && !existing.permissionResolved) { setPendingApprovalSessionId(sessionId); } - // If stream completed while this ChatView was unmounted, consume finalMessageContent now + // If stream completed while this ChatView was unmounted, consume finalMessageContent now. + // Re-fetch messages from DB to avoid duplicates (backend already persisted the reply). if (existing.phase !== 'active' && existing.finalMessageContent) { - const assistantMessage: Message = { - id: 'temp-assistant-' + Date.now(), - session_id: sessionId, - role: 'assistant', - content: existing.finalMessageContent, - created_at: new Date().toISOString(), - token_usage: existing.tokenUsage ? JSON.stringify(existing.tokenUsage) : null, - }; - transferPendingToMessage(assistantMessage.id); - setMessages((prev) => [...prev, assistantMessage]); + detectAssistantCompletion(existing.finalMessageContent); + fetch(`/api/chat/sessions/${sessionId}/messages?limit=50`) + .then(res => res.ok ? res.json() : null) + .then(data => { + if (data?.messages) { + setMessages(data.messages); + } + }) + .catch(() => { + // Fallback: append locally if DB fetch fails + const assistantMessage: Message = { + id: 'temp-assistant-' + Date.now(), + session_id: sessionId, + role: 'assistant', + content: existing.finalMessageContent!, + created_at: new Date().toISOString(), + token_usage: existing.tokenUsage ? JSON.stringify(existing.tokenUsage) : null, + }; + transferPendingToMessage(assistantMessage.id); + setMessages((prev) => [...prev, assistantMessage]); + }); clearSnapshot(sessionId); } } else {