diff --git a/src/components/ChatMessage.tsx b/src/components/ChatMessage.tsx
index 4b507b7..c9f7108 100644
--- a/src/components/ChatMessage.tsx
+++ b/src/components/ChatMessage.tsx
@@ -7,6 +7,7 @@ import remarkBreaks from 'remark-breaks';
import rehypeHighlight from 'rehype-highlight';
import type { ChatMessage as ChatMessageType, MessageBlock } from '../types';
import { ThinkingBlock } from './ThinkingBlock';
+import { ThinkingIndicator } from './ThinkingIndicator';
import { CodeBlock } from './CodeBlock';
import { ToolCall } from './ToolCall';
import { ImageBlock } from './ImageBlock';
@@ -388,14 +389,20 @@ export function ChatMessageComponent({ message, onRetry, agentAvatarUrl }: { mes
{/* Inline images */}
{renderImageBlocks(message.blocks)}
- {/* Streaming dots */}
- {message.isStreaming && (
-
-
-
-
-
- )}
+ {/* Streaming indicator */}
+ {message.isStreaming && (() => {
+ const hasVisibleContent = message.content?.trim();
+ if (!hasVisibleContent) {
+ return ;
+ }
+ return (
+
+
+
+
+
+ );
+ })()}
{/* Tool calls & thinking (inline) */}
{!isUser && }
diff --git a/src/components/ThinkingIndicator.tsx b/src/components/ThinkingIndicator.tsx
new file mode 100644
index 0000000..5cadeeb
--- /dev/null
+++ b/src/components/ThinkingIndicator.tsx
@@ -0,0 +1,42 @@
+import { useState, useEffect, useRef } from 'react';
+import { Brain } from 'lucide-react';
+import { useT } from '../hooks/useLocale';
+
+/**
+ * Animated reasoning/thinking indicator shown during streaming
+ * when no text content has appeared yet (thinking=low mode).
+ * Displays elapsed time and a pulsing animation.
+ */
+export function ThinkingIndicator() {
+ const t = useT();
+ const [elapsed, setElapsed] = useState(0);
+ const startRef = useRef(Date.now());
+
+ useEffect(() => {
+ const interval = setInterval(() => {
+ setElapsed(Math.floor((Date.now() - startRef.current) / 1000));
+ }, 1000);
+ return () => clearInterval(interval);
+ }, []);
+
+ const formatElapsed = (s: number) => {
+ if (s < 60) return `${s}s`;
+ const m = Math.floor(s / 60);
+ const rem = s % 60;
+ return `${m}m ${rem.toString().padStart(2, '0')}s`;
+ };
+
+ return (
+
+
+
+
+ {t('thinking.reasoning')}
+
+
+ {formatElapsed(elapsed)}
+
+
+
+ );
+}
diff --git a/src/lib/i18n.ts b/src/lib/i18n.ts
index 8827705..1e5566e 100644
--- a/src/lib/i18n.ts
+++ b/src/lib/i18n.ts
@@ -60,6 +60,7 @@ const en = {
// Thinking
'thinking.label': 'Thinking',
+ 'thinking.reasoning': 'Reasoning…',
// Tool call
'tool.parameters': 'Parameters',
@@ -151,6 +152,7 @@ const fr: Record = {
'sidebar.deleteCancel': 'Annuler',
'thinking.label': 'Réflexion',
+ 'thinking.reasoning': 'Réflexion…',
'tool.parameters': 'Paramètres',
'tool.result': 'Résultat',