fix: address CodeRabbitAI review comments for conversation history

- Fix duplicate onConversationCreated callbacks by tracking activeConversationId
- Fix history loss when switching conversations with Map-based deduplication
- Disable input while conversation is loading to prevent message routing issues
- Gate WebSocket debug logs behind DEV flag (import.meta.env.DEV)
- Downgrade server logging from info to debug level for reduced noise
- Fix .gitignore prefixes for playwright paths (ui/playwright-report/, ui/test-results/)
- Remove debug console.log from ConversationHistory.tsx
- Add staleTime (30s) to single conversation query for better caching
- Increase history message cap from 20 to 35 for better context
- Replace fixed timeouts with condition-based waits in e2e tests
This commit is contained in:
liri
2026-01-16 22:43:15 +00:00
parent 7d761cb8d0
commit c229e2b39b
8 changed files with 61 additions and 53 deletions

View File

@@ -6,7 +6,7 @@
* Supports conversation history with resume functionality.
*/
import { useState, useRef, useEffect, useCallback } from 'react'
import { useState, useRef, useEffect, useCallback, useMemo } from 'react'
import { Send, Loader2, Wifi, WifiOff, Plus, History } from 'lucide-react'
import { useAssistantChat } from '../hooks/useAssistantChat'
import { ChatMessage as ChatMessageComponent } from './ChatMessage'
@@ -58,21 +58,18 @@ export function AssistantChat({
})
// Notify parent when a NEW conversation is created (not when switching to existing)
// This should only fire when conversationId was null/undefined and a new one was created
const previousConversationIdRef = useRef<number | null | undefined>(conversationId)
// Track activeConversationId to fire callback only once when it transitions from null to a value
const previousActiveConversationIdRef = useRef<number | null>(activeConversationId)
useEffect(() => {
// Only notify if we had NO conversation (null/undefined) and now we have one
// This prevents the bug where switching conversations would trigger this
const hadNoConversation = previousConversationIdRef.current === null || previousConversationIdRef.current === undefined
const nowHasConversation = activeConversationId !== null && activeConversationId !== undefined
const hadNoConversation = previousActiveConversationIdRef.current === null
const nowHasConversation = activeConversationId !== null
if (hadNoConversation && nowHasConversation && onConversationCreated) {
console.log('[AssistantChat] New conversation created:', activeConversationId)
onConversationCreated(activeConversationId)
}
previousConversationIdRef.current = conversationId
}, [activeConversationId, conversationId, onConversationCreated])
previousActiveConversationIdRef.current = activeConversationId
}, [activeConversationId, onConversationCreated])
// Auto-scroll to bottom on new messages
useEffect(() => {
@@ -146,7 +143,7 @@ export function AssistantChat({
const handleSend = () => {
const content = inputValue.trim()
if (!content || isLoading) return
if (!content || isLoading || isLoadingConversation) return
sendMessage(content)
setInputValue('')
@@ -160,23 +157,30 @@ export function AssistantChat({
}
// Combine initial messages (from resumed conversation) with live messages
// Show initialMessages when:
// 1. We have initialMessages from the API
// 2. AND either messages is empty OR we haven't processed this conversation yet
// This prevents showing old conversation messages while switching
const isConversationSynced = lastConversationIdRef.current === conversationId && !isLoadingConversation
const displayMessages = initialMessages && (messages.length === 0 || !isConversationSynced)
? initialMessages
: messages
console.log('[AssistantChat] displayMessages decision:', {
conversationId,
lastRef: lastConversationIdRef.current,
isConversationSynced,
initialMessagesCount: initialMessages?.length ?? 0,
messagesCount: messages.length,
displayMessagesCount: displayMessages.length,
showingInitial: displayMessages === initialMessages
})
// Merge both arrays with deduplication by message ID to prevent history loss
const displayMessages = useMemo(() => {
const isConversationSynced = lastConversationIdRef.current === conversationId && !isLoadingConversation
// If not synced yet, show only initialMessages (or empty)
if (!isConversationSynced) {
return initialMessages ?? []
}
// If no initial messages, just show live messages
if (!initialMessages || initialMessages.length === 0) {
return messages
}
// Merge both arrays, deduplicating by ID (live messages take precedence)
const messageMap = new Map<string, ChatMessage>()
for (const msg of initialMessages) {
messageMap.set(msg.id, msg)
}
for (const msg of messages) {
messageMap.set(msg.id, msg)
}
return Array.from(messageMap.values())
}, [initialMessages, messages, conversationId, isLoadingConversation])
return (
<div className="flex flex-col h-full">
@@ -288,7 +292,7 @@ export function AssistantChat({
onChange={(e) => setInputValue(e.target.value)}
onKeyDown={handleKeyDown}
placeholder="Ask about the codebase..."
disabled={isLoading || connectionStatus !== 'connected'}
disabled={isLoading || isLoadingConversation || connectionStatus !== 'connected'}
className="
flex-1
neo-input
@@ -301,7 +305,7 @@ export function AssistantChat({
/>
<button
onClick={handleSend}
disabled={!inputValue.trim() || isLoading || connectionStatus !== 'connected'}
disabled={!inputValue.trim() || isLoading || isLoadingConversation || connectionStatus !== 'connected'}
className="
neo-btn neo-btn-primary
px-4

View File

@@ -76,7 +76,6 @@ export function ConversationHistory({
}
const handleSelectConversation = (conversationId: number) => {
console.log('[ConversationHistory] handleSelectConversation called with id:', conversationId)
onSelectConversation(conversationId)
onClose()
}
@@ -129,11 +128,6 @@ export function ConversationHistory({
<div className="max-h-[300px] overflow-auto">
{conversations.map((conversation) => {
const isCurrent = conversation.id === currentConversationId
console.log('[ConversationHistory] Rendering conversation:', {
id: conversation.id,
currentConversationId,
isCurrent
})
return (
<div

View File

@@ -120,7 +120,9 @@ export function useAssistantChat({
ws.onmessage = (event) => {
try {
const data = JSON.parse(event.data) as AssistantChatServerMessage;
console.log('[useAssistantChat] Received WebSocket message:', data.type, data);
if (import.meta.env.DEV) {
console.debug('[useAssistantChat] Received WebSocket message:', data.type, data);
}
switch (data.type) {
case "text": {
@@ -278,7 +280,9 @@ export function useAssistantChat({
payload.conversation_id = existingConversationId;
setConversationId(existingConversationId);
}
console.log('[useAssistantChat] Sending start message:', payload);
if (import.meta.env.DEV) {
console.debug('[useAssistantChat] Sending start message:', payload);
}
wsRef.current.send(JSON.stringify(payload));
} else if (wsRef.current?.readyState === WebSocket.CONNECTING) {
checkAndSendTimeoutRef.current = window.setTimeout(checkAndSend, 100);

View File

@@ -25,6 +25,7 @@ export function useConversation(projectName: string | null, conversationId: numb
queryKey: ['conversation', projectName, conversationId],
queryFn: () => api.getAssistantConversation(projectName!, conversationId!),
enabled: !!projectName && !!conversationId,
staleTime: 30_000, // Cache for 30 seconds
})
}