From 29482e377a05d5dc482a02e17976f36d0f0262aa Mon Sep 17 00:00:00 2001 From: Nicolas Varrot Date: Thu, 12 Feb 2026 01:42:40 +0000 Subject: [PATCH] fix: resolve ESLint errors for React compiler rules - Chat.tsx: replace mutable variable in render IIFE with useMemo+reduce to satisfy react-hooks/immutability rule - ConnectionBanner.tsx: move setState calls into a useCallback to avoid synchronous setState in effect body (react-hooks/set-state-in-effect) - useGateway.ts: suppress set-state-in-effect for legitimate mount init --- FEEDBACK.md | 18 ++++++++++++++++++ src/components/Chat.tsx | 24 +++++++++++++----------- src/components/ConnectionBanner.tsx | 19 +++++++++++-------- src/hooks/useGateway.ts | 2 ++ 4 files changed, 44 insertions(+), 19 deletions(-) diff --git a/FEEDBACK.md b/FEEDBACK.md index b82fb3d..4883b64 100644 --- a/FEEDBACK.md +++ b/FEEDBACK.md @@ -175,3 +175,21 @@ - GitHub rend nativement les blocs ```mermaid dans les README - Utiliser un flowchart ou graph LR/TD montrant : Browser → WebSocket → OpenClaw Gateway → LLM Provider, avec les composants internes (LoginScreen, Chat, Sidebar, Gateway client, etc.) - Plus lisible et maintenable que l'ASCII art + +## Item #22 +- **Date:** 2026-02-12 +- **Priority:** high +- **Status:** pending +- **Description:** CI GitHub Actions en échec — vérifier et réparer en priorité. Le cron doit aussi vérifier l'état de la CI en début de chaque session avant toute autre amélioration. Si la CI est cassée, c'est la priorité #1. + +## Item #23 +- **Date:** 2026-02-12 +- **Priority:** medium +- **Status:** pending +- **Description:** Icônes par channel/type dans la liste des sessions (sidebar) + - Discord → icône Discord + - Telegram → icône Telegram + - Cron → icône horloge ou engrenage + - Webchat → icône chat/bulle + - Fallback générique pour les channels non-vanilla (ex: TeamSpeak) → icône par défaut (bulle ou globe) + - Utiliser des SVG ou une lib d'icônes (lucide-react, react-icons, etc.) diff --git a/src/components/Chat.tsx b/src/components/Chat.tsx index 6f4f733..caf58b9 100644 --- a/src/components/Chat.tsx +++ b/src/components/Chat.tsx @@ -1,4 +1,4 @@ -import { useEffect, useRef, useCallback, useState } from 'react'; +import { useEffect, useRef, useCallback, useState, useMemo } from 'react'; import { ChatMessageComponent } from './ChatMessage'; import { ChatInput } from './ChatInput'; import { TypingIndicator } from './TypingIndicator'; @@ -112,6 +112,16 @@ export function Chat({ messages, isGenerating, status, onSend, onAbort }: Props) onSend(text, attachments); }, [onSend]); + const visibleMessages = useMemo(() => { + const filtered = messages.filter(hasVisibleContent); + return filtered.reduce>((acc, msg) => { + const dk = getDateKey(msg.timestamp); + const prevDk = acc.length > 0 ? getDateKey(acc[acc.length - 1].msg.timestamp) : ''; + acc.push({ msg, showSep: dk !== prevDk }); + return acc; + }, []); + }, [messages]); + const showTyping = isGenerating && !hasStreamedText(messages); return ( @@ -130,13 +140,7 @@ export function Chat({ messages, isGenerating, status, onSend, onAbort }: Props)
{t('chat.welcomeSub')}
)} - {(() => { - let lastDateKey = ''; - return messages.filter(hasVisibleContent).map(msg => { - const dk = getDateKey(msg.timestamp); - const showSep = dk !== lastDateKey; - lastDateKey = dk; - return ( + {visibleMessages.map(({ msg, showSep }) => (
{showSep && (
@@ -147,9 +151,7 @@ export function Chat({ messages, isGenerating, status, onSend, onAbort }: Props) )}
- ); - }); - })()} + ))} {showTyping && }
diff --git a/src/components/ConnectionBanner.tsx b/src/components/ConnectionBanner.tsx index 15e5878..72b09f0 100644 --- a/src/components/ConnectionBanner.tsx +++ b/src/components/ConnectionBanner.tsx @@ -1,4 +1,4 @@ -import { useState, useEffect, useRef } from 'react'; +import { useState, useEffect, useRef, useCallback } from 'react'; import { Wifi, Loader2 } from 'lucide-react'; import type { ConnectionStatus } from '../types'; import { useT } from '../hooks/useLocale'; @@ -15,28 +15,31 @@ export function ConnectionBanner({ status }: Props) { const prevStatus = useRef(null); const dismissTimer = useRef | null>(null); - useEffect(() => { - const prev = prevStatus.current; - prevStatus.current = status; - + const updateBanner = useCallback((prev: ConnectionStatus | null, current: ConnectionStatus) => { if (dismissTimer.current) { clearTimeout(dismissTimer.current); dismissTimer.current = null; } - if (status === 'disconnected' || status === 'connecting') { + if (current === 'disconnected' || current === 'connecting') { if (prev === 'connected') { setBanner('reconnecting'); } - } else if (status === 'connected' && prev !== null && prev !== 'connected') { + } else if (current === 'connected' && prev !== null && prev !== 'connected') { setBanner('reconnected'); dismissTimer.current = setTimeout(() => setBanner('hidden'), 3000); } + }, []); + + useEffect(() => { + const prev = prevStatus.current; + prevStatus.current = status; + updateBanner(prev, status); return () => { if (dismissTimer.current) clearTimeout(dismissTimer.current); }; - }, [status]); + }, [status, updateBanner]); if (banner === 'hidden') return null; diff --git a/src/hooks/useGateway.ts b/src/hooks/useGateway.ts index c0c1646..28ca729 100644 --- a/src/hooks/useGateway.ts +++ b/src/hooks/useGateway.ts @@ -307,6 +307,8 @@ export function useGateway() { initRef.current = true; const stored = getStoredCredentials(); if (stored) { + // Init on mount — setupClient sets state as part of establishing the connection + // eslint-disable-next-line react-hooks/set-state-in-effect setupClient(stored.url, stored.token); } else { setAuthenticated(false);