From 2b1ca2d0c872d3e851bb1c78de82b5c517b79831 Mon Sep 17 00:00:00 2001 From: Nicolas Varrot Date: Thu, 12 Feb 2026 19:36:53 +0000 Subject: [PATCH] feat: add collapse/expand all tool calls toggle button Adds a floating button in the chat area that lets users collapse or expand all tool call details at once. Useful for long conversations with many tool calls where scrolling through expanded results is tedious. - New ToolCollapseContext provides global collapse/expand state - ToolCall components react to global state changes via version tracking - Toggle button appears only when conversation has tool calls - Supports EN/FR i18n --- src/App.tsx | 3 ++ src/components/Chat.tsx | 26 ++++++++++++---- src/components/ToolCall.tsx | 14 ++++++++- src/contexts/ToolCollapseContext.tsx | 44 ++++++++++++++++++++++++++++ src/lib/i18n.ts | 4 +++ 5 files changed, 84 insertions(+), 7 deletions(-) create mode 100644 src/contexts/ToolCollapseContext.tsx 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…',