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…',