diff --git a/src/App.tsx b/src/App.tsx
index 21f009f..2ecd6a9 100644
--- a/src/App.tsx
+++ b/src/App.tsx
@@ -6,6 +6,7 @@ import { Sidebar } from './components/Sidebar';
import { LoginScreen } from './components/LoginScreen';
import { ConnectionBanner } from './components/ConnectionBanner';
import { KeyboardShortcuts } from './components/KeyboardShortcuts';
+import { ToolCollapseProvider } from './contexts/ToolCollapseContext';
const Chat = lazy(() => import('./components/Chat').then(m => ({ default: m.Chat })));
@@ -85,6 +86,7 @@ export default function App() {
}
return (
+
setShortcutsOpen(false)} />
+
);
}
diff --git a/src/components/Chat.tsx b/src/components/Chat.tsx
index d954533..2953c6a 100644
--- a/src/components/Chat.tsx
+++ b/src/components/Chat.tsx
@@ -3,9 +3,10 @@ import { ChatMessageComponent } from './ChatMessage';
import { ChatInput } from './ChatInput';
import { TypingIndicator } from './TypingIndicator';
import type { ChatMessage, ConnectionStatus } from '../types';
-import { Bot, ArrowDown, Loader2 } from 'lucide-react';
+import { Bot, ArrowDown, Loader2, ChevronsDownUp, ChevronsUpDown } from 'lucide-react';
import { useT } from '../hooks/useLocale';
import { getLocale, type TranslationKey } from '../lib/i18n';
+import { useToolCollapse } from '../contexts/ToolCollapseContext';
interface Props {
messages: ChatMessage[];
@@ -126,6 +127,9 @@ export function Chat({ messages, isGenerating, isLoadingHistory, status, session
const showTyping = isGenerating && !hasStreamedText(messages);
+ const { globalState, collapseAll, expandAll } = useToolCollapse();
+ const hasToolCalls = useMemo(() => messages.some(m => m.blocks.some(b => b.type === 'tool_use' || b.type === 'tool_result')), [messages]);
+
return (
@@ -164,9 +168,19 @@ export function Chat({ messages, isGenerating, isLoadingHistory, status, session
- {/* Scroll to bottom FAB */}
- {showScrollBtn && (
-
+ {/* Floating action buttons */}
+
+ {hasToolCalls && (
+
+ )}
+ {showScrollBtn && (
-
- )}
+ )}
+
);
diff --git a/src/components/ToolCall.tsx b/src/components/ToolCall.tsx
index b2498ac..a6460c0 100644
--- a/src/components/ToolCall.tsx
+++ b/src/components/ToolCall.tsx
@@ -1,8 +1,9 @@
-import { useState, useCallback, useMemo } from 'react';
+import { useState, useCallback, useMemo, useEffect, useRef } from 'react';
import { ChevronRight, ChevronDown, Check, Copy } from 'lucide-react';
import hljs from 'highlight.js/lib/common';
import { useT } from '../hooks/useLocale';
import { ImageBlock } from './ImageBlock';
+import { useToolCollapse } from '../contexts/ToolCollapseContext';
type ToolColor = { border: string; bg: string; text: string; icon: string; glow: string; expandBorder: string; expandBg: string };
@@ -217,8 +218,19 @@ function extractImageFromResult(result: string): { src: string; remaining: strin
export function ToolCall({ name, input, result }: { name: string; input?: Record; result?: string }) {
const t = useT();
const [open, setOpen] = useState(false);
+ const { globalState, version } = useToolCollapse();
+ const lastVersion = useRef(version);
const c = getColor(name);
+ // Respond to global collapse/expand commands
+ useEffect(() => {
+ if (version !== lastVersion.current) {
+ lastVersion.current = version;
+ if (globalState === 'collapse-all') setOpen(false);
+ else if (globalState === 'expand-all') setOpen(true);
+ }
+ }, [globalState, version]);
+
const inputStr = input ? (typeof input === 'string' ? input : JSON.stringify(input, null, 2)) : '';
const hint = getContextHint(name, input);
diff --git a/src/contexts/ToolCollapseContext.tsx b/src/contexts/ToolCollapseContext.tsx
new file mode 100644
index 0000000..2a49a9c
--- /dev/null
+++ b/src/contexts/ToolCollapseContext.tsx
@@ -0,0 +1,44 @@
+import { createContext, useContext, useState, useCallback, type ReactNode } from 'react';
+
+type ToolCollapseState = 'none' | 'collapse-all' | 'expand-all';
+
+interface ToolCollapseContextValue {
+ /** Global override: 'none' means each tool manages its own state */
+ globalState: ToolCollapseState;
+ /** Monotonically increasing version — tool calls reset local state when this changes */
+ version: number;
+ collapseAll: () => void;
+ expandAll: () => void;
+}
+
+const ToolCollapseContext = createContext({
+ globalState: 'none',
+ version: 0,
+ collapseAll: () => {},
+ expandAll: () => {},
+});
+
+export function ToolCollapseProvider({ children }: { children: ReactNode }) {
+ const [globalState, setGlobalState] = useState('none');
+ const [version, setVersion] = useState(0);
+
+ const collapseAll = useCallback(() => {
+ setGlobalState('collapse-all');
+ setVersion(v => v + 1);
+ }, []);
+
+ const expandAll = useCallback(() => {
+ setGlobalState('expand-all');
+ setVersion(v => v + 1);
+ }, []);
+
+ return (
+
+ {children}
+
+ );
+}
+
+export function useToolCollapse() {
+ return useContext(ToolCollapseContext);
+}
diff --git a/src/lib/i18n.ts b/src/lib/i18n.ts
index cb74463..caa08fa 100644
--- a/src/lib/i18n.ts
+++ b/src/lib/i18n.ts
@@ -41,6 +41,8 @@ const en = {
'chat.send': 'Send',
'chat.stop': 'Stop',
'chat.scrollToBottom': 'New messages',
+ 'chat.collapseTools': 'Collapse all tools',
+ 'chat.expandTools': 'Expand all tools',
'chat.messages': 'Chat messages',
'chat.thinking': 'Thinking…',
@@ -131,6 +133,8 @@ const fr: Record = {
'chat.send': 'Envoyer',
'chat.stop': 'Arrêter',
'chat.scrollToBottom': 'Nouveaux messages',
+ 'chat.collapseTools': 'Replier tous les outils',
+ 'chat.expandTools': 'Déplier tous les outils',
'chat.messages': 'Messages du chat',
'chat.thinking': 'Réflexion…',