Skip to content

Commit c07a604

Browse files
7418claude
andcommitted
fix: address code review findings — snapshot dedup, workspace hooks, permission params, Windows health check
ChatView.tsx (P2): snapshot consumption now calls detectAssistantCompletion() so assistant workspace onboarding/check-in state updates even when the stream completes while ChatView is unmounted. ChatView.tsx (P2): replace local temp-message append with a DB re-fetch on snapshot consumption to avoid duplicate messages (backend already persists the reply in route.ts). page.tsx (P3): add updatedInput and denyMessage parameters to handlePermissionResponse so AskUserQuestion answers and ExitPlanMode custom deny messages are forwarded to the permission API on the first-message page. electron/main.ts: fix Windows startup timeout by forcing IPv4 (family:4) in health check HTTP request and improving error diagnostics. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 5eedb5b commit c07a604

File tree

3 files changed

+45
-21
lines changed

3 files changed

+45
-21
lines changed

electron/main.ts

Lines changed: 18 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -361,6 +361,7 @@ function getPort(): Promise<number> {
361361

362362
async function waitForServer(port: number, timeout = 30000): Promise<void> {
363363
const start = Date.now();
364+
let lastError = '';
364365
while (Date.now() - start < timeout) {
365366
// If the server process already exited, fail fast
366367
if (serverExited) {
@@ -371,23 +372,33 @@ async function waitForServer(port: number, timeout = 30000): Promise<void> {
371372
try {
372373
await new Promise<void>((resolve, reject) => {
373374
// eslint-disable-next-line @typescript-eslint/no-require-imports
374-
const req = require('http').get(`http://127.0.0.1:${port}/api/health`, (res: { statusCode?: number }) => {
375+
const http = require('http');
376+
// Use options object with family:4 to force IPv4 — avoids Windows
377+
// IPv6 resolution issues where 127.0.0.1 may fail to connect.
378+
const req = http.get({
379+
hostname: '127.0.0.1',
380+
port,
381+
path: '/api/health',
382+
family: 4,
383+
timeout: 2000,
384+
}, (res: { statusCode?: number }) => {
375385
if (res.statusCode === 200) resolve();
376386
else reject(new Error(`Status ${res.statusCode}`));
377387
});
378-
req.on('error', reject);
379-
req.setTimeout(1000, () => {
388+
req.on('error', (err: Error) => reject(err));
389+
req.on('timeout', () => {
380390
req.destroy();
381-
reject(new Error('timeout'));
391+
reject(new Error('request timeout'));
382392
});
383393
});
384394
return;
385-
} catch {
386-
await new Promise(r => setTimeout(r, 200));
395+
} catch (err) {
396+
lastError = err instanceof Error ? err.message : String(err);
397+
await new Promise(r => setTimeout(r, 300));
387398
}
388399
}
389400
throw new Error(
390-
`Server startup timeout after ${timeout / 1000}s.\n\n${serverErrors.length > 0 ? 'Server output:\n' + serverErrors.slice(-10).join('\n') : 'No server output captured.'}`
401+
`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.'}`
391402
);
392403
}
393404

src/app/chat/page.tsx

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -42,15 +42,16 @@ export default function NewChatPage() {
4242
abortControllerRef.current = null;
4343
}, []);
4444

45-
const handlePermissionResponse = useCallback(async (decision: 'allow' | 'allow_session' | 'deny') => {
45+
const handlePermissionResponse = useCallback(async (decision: 'allow' | 'allow_session' | 'deny', updatedInput?: Record<string, unknown>, denyMessage?: string) => {
4646
if (!pendingPermission) return;
4747

48-
const body: { permissionRequestId: string; decision: { behavior: 'allow'; updatedPermissions?: unknown[] } | { behavior: 'deny'; message?: string } } = {
48+
const body: { permissionRequestId: string; decision: { behavior: 'allow'; updatedInput?: Record<string, unknown>; updatedPermissions?: unknown[] } | { behavior: 'deny'; message?: string } } = {
4949
permissionRequestId: pendingPermission.permissionRequestId,
5050
decision: decision === 'deny'
51-
? { behavior: 'deny', message: 'User denied permission' }
51+
? { behavior: 'deny', message: denyMessage || 'User denied permission' }
5252
: {
5353
behavior: 'allow',
54+
...(updatedInput ? { updatedInput } : {}),
5455
...(decision === 'allow_session' && pendingPermission.suggestions
5556
? { updatedPermissions: pendingPermission.suggestions }
5657
: {}),

src/components/chat/ChatView.tsx

Lines changed: 23 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -118,18 +118,30 @@ export function ChatView({ sessionId, initialMessages = [], initialHasMore = fal
118118
if (existing.pendingPermission && !existing.permissionResolved) {
119119
setPendingApprovalSessionId(sessionId);
120120
}
121-
// If stream completed while this ChatView was unmounted, consume finalMessageContent now
121+
// If stream completed while this ChatView was unmounted, consume finalMessageContent now.
122+
// Re-fetch messages from DB to avoid duplicates (backend already persisted the reply).
122123
if (existing.phase !== 'active' && existing.finalMessageContent) {
123-
const assistantMessage: Message = {
124-
id: 'temp-assistant-' + Date.now(),
125-
session_id: sessionId,
126-
role: 'assistant',
127-
content: existing.finalMessageContent,
128-
created_at: new Date().toISOString(),
129-
token_usage: existing.tokenUsage ? JSON.stringify(existing.tokenUsage) : null,
130-
};
131-
transferPendingToMessage(assistantMessage.id);
132-
setMessages((prev) => [...prev, assistantMessage]);
124+
detectAssistantCompletion(existing.finalMessageContent);
125+
fetch(`/api/chat/sessions/${sessionId}/messages?limit=50`)
126+
.then(res => res.ok ? res.json() : null)
127+
.then(data => {
128+
if (data?.messages) {
129+
setMessages(data.messages);
130+
}
131+
})
132+
.catch(() => {
133+
// Fallback: append locally if DB fetch fails
134+
const assistantMessage: Message = {
135+
id: 'temp-assistant-' + Date.now(),
136+
session_id: sessionId,
137+
role: 'assistant',
138+
content: existing.finalMessageContent!,
139+
created_at: new Date().toISOString(),
140+
token_usage: existing.tokenUsage ? JSON.stringify(existing.tokenUsage) : null,
141+
};
142+
transferPendingToMessage(assistantMessage.id);
143+
setMessages((prev) => [...prev, assistantMessage]);
144+
});
133145
clearSnapshot(sessionId);
134146
}
135147
} else {

0 commit comments

Comments
 (0)