Repository: All-Hands-AI/OpenHands
File: frontend/src/contexts/conversation-websocket-context.tsx (TypeScript)

Code before:
  );

  // Reset hasConnected flags and history loading state when conversation changes
  useEffect(() => {
    hasConnectedRefPlanning.current = false;
    setIsLoadingHistoryMain(true);
    setExpectedEventCountMain(null);
    receivedEventCountRefMain.current = 0;
    // Reset the tracked event ref when conversation changes
    latestPlanningFileEventRef.current = null;
  }, [conversationId]);

  const { data: preloadedEvents } = useConversationHistory(conversationId);

  useEffect(() => {
    if (!preloadedEvents || preloadedEvents.length === 0) return;

    useEventStore.getState().clearEvents();

    for (const event of preloadedEvents) {
      addEvent(event as OHEvent);
    }

    setIsLoadingHistoryMain(false);
  }, [preloadedEvents, addEvent]);

  // Separate message handlers for each connection
  const handleMainMessage = useCallback(
    (messageEvent: MessageEvent) => {
      try {
        const event = JSON.parse(messageEvent.data);

        // Track received events for history loading (count ALL events from WebSocket)
        // Always count when loading, even if we don't have the expected count yet
        if (isLoadingHistoryMain) {
          receivedEventCountRefMain.current += 1;

          if (
            expectedEventCountMain !== null &&
            receivedEventCountRefMain.current >= expectedEventCountMain
          ) {
            setIsLoadingHistoryMain(false);
          }
        }

        // Use type guard to validate v1 event structure
        if (isV1Event(event)) {
          addEvent(event);

          // Handle ConversationErrorEvent specifically - show error banner
          // AgentErrorEvent errors are displayed inline in the chat, not as banners

Review comment (bug):
```suggestion
  useEffect(() => {
    if (!preloadedEvents || preloadedEvents.length === 0) {
      setIsLoadingHistoryMain(false);
      return;
    }

    for (const event of preloadedEvents) {
      addEvent(event);
    }

    setIsLoadingHistoryMain(false);
  }, [preloadedEvents, addEvent]);
```

`useEventStore` already handles duplicate events, so it should be safe to remove it and avoid possible race conditions.

Code after:
  );

  // Reset hasConnected flags and history loading state when conversation changes
  useEffect(() => {
    hasConnectedRefPlanning.current = false;
    setIsLoadingHistoryMain(true);
    setExpectedEventCountMain(null);
    receivedEventCountRefMain.current = 0;
    // Reset the tracked event ref when conversation changes
    latestPlanningFileEventRef.current = null;
  }, [conversationId]);

  const { data: preloadedEvents } = useConversationHistory(conversationId);

  useEffect(() => {
    if (!preloadedEvents || preloadedEvents.length === 0) {
      setIsLoadingHistoryMain(false);
      return;
    }

    for (const event of preloadedEvents) {
      addEvent(event);
    }

    setIsLoadingHistoryMain(false);
  }, [preloadedEvents, addEvent]);

  // Separate message handlers for each connection
  const handleMainMessage = useCallback(
    (messageEvent: MessageEvent) => {
      try {
        const event = JSON.parse(messageEvent.data);

        // Track received events for history loading (count ALL events from WebSocket)
        // Always count when loading, even if we don't have the expected count yet
        if (isLoadingHistoryMain) {
          receivedEventCountRefMain.current += 1;

          if (
            expectedEventCountMain !== null &&
            receivedEventCountRefMain.current >= expectedEventCountMain
          ) {
            setIsLoadingHistoryMain(false);
          }
        }

        // Use type guard to validate v1 event structure
        if (isV1Event(event)) {
          addEvent(event);

          // Handle ConversationErrorEvent specifically - show error banner