fix: address second round of code review feedback

Backend improvements:
- Create shared validation utility for project name validation
- Add asyncio.Lock to prevent concurrent _query_claude calls
- Fix _create_features_bulk: use flush() for IDs, add rollback on error
- Use unique temp settings file instead of overwriting .claude_settings.json
- Remove exception details from error messages (security)

Frontend improvements:
- Memoize onError callback in ExpandProjectChat for stable dependencies
- Add timeout to start() checkAndSend loop to prevent infinite retries
- Add manuallyDisconnectedRef to prevent reconnection after explicit disconnect
- Clear pending reconnect timeout in disconnect()

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
Dan Gentry
2026-01-09 23:57:50 -05:00
parent 75f2bf2a10
commit cdcbd11272
7 changed files with 106 additions and 53 deletions

View File

@@ -34,6 +34,9 @@ export function ExpandProjectChat({
const inputRef = useRef<HTMLInputElement>(null)
const fileInputRef = useRef<HTMLInputElement>(null)
// Memoize error handler to keep hook dependencies stable
const handleError = useCallback((err: string) => setError(err), [])
const {
messages,
isLoading,
@@ -46,7 +49,7 @@ export function ExpandProjectChat({
} = useExpandChat({
projectName,
onComplete,
onError: (err) => setError(err),
onError: handleError,
})
// Start the chat session when component mounts

View File

@@ -54,6 +54,7 @@ export function useExpandChat({
const pingIntervalRef = useRef<number | null>(null)
const reconnectTimeoutRef = useRef<number | null>(null)
const isCompleteRef = useRef(false)
const manuallyDisconnectedRef = useRef(false)
// Keep isCompleteRef in sync with isComplete state
useEffect(() => {
@@ -76,6 +77,10 @@ export function useExpandChat({
}, [])
const connect = useCallback(() => {
// Don't reconnect if manually disconnected
if (manuallyDisconnectedRef.current) {
return
}
if (wsRef.current?.readyState === WebSocket.OPEN) {
return
}
@@ -92,6 +97,7 @@ export function useExpandChat({
ws.onopen = () => {
setConnectionStatus('connected')
reconnectAttempts.current = 0
manuallyDisconnectedRef.current = false
// Start ping interval to keep connection alive
pingIntervalRef.current = window.setInterval(() => {
@@ -109,7 +115,11 @@ export function useExpandChat({
}
// Attempt reconnection if not intentionally closed
if (reconnectAttempts.current < maxReconnectAttempts && !isCompleteRef.current) {
if (
!manuallyDisconnectedRef.current &&
reconnectAttempts.current < maxReconnectAttempts &&
!isCompleteRef.current
) {
reconnectAttempts.current++
const delay = Math.min(1000 * Math.pow(2, reconnectAttempts.current), 10000)
reconnectTimeoutRef.current = window.setTimeout(connect, delay)
@@ -244,18 +254,25 @@ export function useExpandChat({
const start = useCallback(() => {
connect()
// Wait for connection then send start message
// Wait for connection then send start message (with timeout to prevent infinite loop)
let attempts = 0
const maxAttempts = 50 // 5 seconds max (50 * 100ms)
const checkAndSend = () => {
if (wsRef.current?.readyState === WebSocket.OPEN) {
setIsLoading(true)
wsRef.current.send(JSON.stringify({ type: 'start' }))
} else if (wsRef.current?.readyState === WebSocket.CONNECTING) {
setTimeout(checkAndSend, 100)
if (attempts++ < maxAttempts) {
setTimeout(checkAndSend, 100)
} else {
onError?.('Connection timeout')
setIsLoading(false)
}
}
}
setTimeout(checkAndSend, 100)
}, [connect])
}, [connect, onError])
const sendMessage = useCallback((content: string, attachments?: ImageAttachment[]) => {
if (!wsRef.current || wsRef.current.readyState !== WebSocket.OPEN) {
@@ -297,6 +314,7 @@ export function useExpandChat({
}, [onError])
const disconnect = useCallback(() => {
manuallyDisconnectedRef.current = true
reconnectAttempts.current = maxReconnectAttempts // Prevent reconnection
if (pingIntervalRef.current) {
clearInterval(pingIntervalRef.current)