diff --git a/app/components/chat/AssistantMessage.tsx b/app/components/chat/AssistantMessage.tsx index c9ecdba..7c39276 100644 --- a/app/components/chat/AssistantMessage.tsx +++ b/app/components/chat/AssistantMessage.tsx @@ -176,6 +176,9 @@ export const AssistantMessage = memo( + + {content} + {toolInvocations && toolInvocations.length > 0 && ( )} - - {content} - ); }, diff --git a/app/components/chat/ToolInvocations.tsx b/app/components/chat/ToolInvocations.tsx index cb0e145..e61a8e0 100644 --- a/app/components/chat/ToolInvocations.tsx +++ b/app/components/chat/ToolInvocations.tsx @@ -1,6 +1,6 @@ import type { ToolInvocationUIPart } from '@ai-sdk/ui-utils'; import { AnimatePresence, motion } from 'framer-motion'; -import { memo, useMemo, useState } from 'react'; +import { memo, useMemo, useState, useEffect } from 'react'; import { createHighlighter, type BundledLanguage, type BundledTheme, type HighlighterGeneric } from 'shiki'; import { classNames } from '~/utils/classNames'; import { @@ -102,18 +102,17 @@ export const ToolInvocations = memo(({ toolInvocations, toolCallAnnotations, add } return ( -
+
-
{hasToolResults && (
-
+
{ + const [expanded, setExpanded] = useState<{ [id: string]: boolean }>({}); + + // OS detection for shortcut display + const isMac = typeof navigator !== 'undefined' && /Mac|iPod|iPhone|iPad/.test(navigator.platform); + + const toggleExpand = (toolCallId: string) => { + setExpanded((prev) => ({ + ...prev, + [toolCallId]: !prev[toolCallId], + })); + }; + + useEffect(() => { + const expandedState: { [id: string]: boolean } = {}; + toolInvocations.forEach((inv) => { + if (inv.toolInvocation.state === 'call') { + expandedState[inv.toolInvocation.toolCallId] = true; + } + }); + setExpanded(expandedState); + }, [toolInvocations]); + + // Keyboard shortcut logic + useEffect(() => { + const handleKeyDown = (e: KeyboardEvent) => { + // Ignore if focus is in an input/textarea/contenteditable + const active = document.activeElement as HTMLElement | null; + + if (active && (active.tagName === 'INPUT' || active.tagName === 'TEXTAREA' || active.isContentEditable)) { + return; + } + + if (Object.keys(expanded).length === 0) { + return; + } + + const openId = Object.keys(expanded).find((id) => expanded[id]); + + if (!openId) { + return; + } + + // Cancel: Cmd/Ctrl + Backspace + if ((isMac ? e.metaKey : e.ctrlKey) && e.key === 'Backspace') { + e.preventDefault(); + addToolResult({ + toolCallId: openId, + result: TOOL_EXECUTION_APPROVAL.REJECT, + }); + } + + // Run tool: Cmd/Ctrl + Enter + if ((isMac ? e.metaKey : e.ctrlKey) && (e.key === 'Enter' || e.key === 'Return')) { + e.preventDefault(); + addToolResult({ + toolCallId: openId, + result: TOOL_EXECUTION_APPROVAL.APPROVE, + }); + } + }; + window.addEventListener('keydown', handleKeyDown); + + return () => window.removeEventListener('keydown', handleKeyDown); + }, [expanded, addToolResult, isMac]); + return (
    @@ -287,10 +350,7 @@ const ToolCallsList = memo(({ toolInvocations, toolCallAnnotations, addToolResul } const { toolName, toolCallId } = tool.toolInvocation; - - const annotation = toolCallAnnotations.find((annotation) => { - return annotation.toolCallId === toolCallId; - }); + const annotation = toolCallAnnotations.find((annotation) => annotation.toolCallId === toolCallId); return ( -
    -
    -
    Bolt wants to use a tool.
    -
    - Server:{' '} - {annotation?.serverName} -
    -
    - Tool: {toolName} -
    -
    - Description:{' '} - {annotation?.toolDescription} -
    -
    Parameters:
    -
    - -
    {' '} -
    +
    +
    +
    + Calling MCP tool{' '} + + {toolName} + +
    + {expanded[toolCallId] && ( +
    +
    +
    +
    + Description:{' '} + + {annotation?.toolDescription} + +
    +
    +
    + +
    +
    +
    +
    + )} +
    +