diff --git a/gui/src/components/mainInput/ContinueInputBox.tsx b/gui/src/components/mainInput/ContinueInputBox.tsx index 457aefd449..14d4816f43 100644 --- a/gui/src/components/mainInput/ContinueInputBox.tsx +++ b/gui/src/components/mainInput/ContinueInputBox.tsx @@ -5,7 +5,7 @@ import styled, { keyframes } from "styled-components"; import { defaultBorderRadius, vscBackground } from ".."; import { useAppSelector } from "../../redux/hooks"; import { selectSlashCommandComboBoxInputs } from "../../redux/selectors"; -import ContextItemsPeek from "./belowMainInput/ContextItemsPeek"; +import { ContextItemsPeek } from "./belowMainInput/ContextItemsPeek"; import { ToolbarOptions } from "./InputToolbar"; import { Lump } from "./Lump"; import { TipTapEditor } from "./TipTapEditor"; diff --git a/gui/src/components/mainInput/belowMainInput/ContextItemsPeek.tsx b/gui/src/components/mainInput/belowMainInput/ContextItemsPeek.tsx index 5840240c2e..7c67463a96 100644 --- a/gui/src/components/mainInput/belowMainInput/ContextItemsPeek.tsx +++ b/gui/src/components/mainInput/belowMainInput/ContextItemsPeek.tsx @@ -18,14 +18,15 @@ interface ContextItemsPeekProps { isCurrentContextPeek: boolean; icon?: ComponentType>; title?: JSX.Element | string; - showWhenNoResults?: boolean; } interface ContextItemsPeekItemProps { contextItem: ContextItemWithId; } -function ContextItemsPeekItem({ contextItem }: ContextItemsPeekItemProps) { +export function ContextItemsPeekItem({ + contextItem, +}: ContextItemsPeekItemProps) { const ideMessenger = useContext(IdeMessengerContext); const isUrl = contextItem.uri?.type === "url"; @@ -150,12 +151,11 @@ function ContextItemsPeekItem({ contextItem }: ContextItemsPeekItemProps) { ); } -function ContextItemsPeek({ +export function ContextItemsPeek({ contextItems, isCurrentContextPeek, icon, title, - showWhenNoResults, }: ContextItemsPeekProps) { const ctxItems = useMemo(() => { return contextItems?.filter((ctxItem) => !ctxItem.hidden) ?? []; @@ -165,11 +165,7 @@ function ContextItemsPeek({ const indicateIsGathering = isCurrentContextPeek && isGatheringContext; - if ( - !showWhenNoResults && - (!ctxItems || ctxItems.length === 0) && - !indicateIsGathering - ) { + if ((!ctxItems || ctxItems.length === 0) && !indicateIsGathering) { return null; } @@ -198,5 +194,3 @@ function ContextItemsPeek({ ); } - -export default ContextItemsPeek; diff --git a/gui/src/pages/gui/ToolCallDiv/SimpleToolCallUI.tsx b/gui/src/pages/gui/ToolCallDiv/SimpleToolCallUI.tsx new file mode 100644 index 0000000000..f88b6fdac6 --- /dev/null +++ b/gui/src/pages/gui/ToolCallDiv/SimpleToolCallUI.tsx @@ -0,0 +1,97 @@ +import { ChevronDownIcon, ChevronRightIcon } from "@heroicons/react/24/outline"; +import { ContextItemWithId, Tool, ToolCallState } from "core"; +import { ComponentType, useMemo, useState } from "react"; +import { ContextItemsPeekItem } from "../../../components/mainInput/belowMainInput/ContextItemsPeek"; +import { ArgsItems, ArgsToggleIcon } from "./ToolCallArgs"; +import { ToolCallStatusMessage } from "./ToolCallStatusMessage"; + +interface SimpleToolCallUIProps { + toolCallState: ToolCallState; + tool: Tool | undefined; + contextItems: ContextItemWithId[]; + icon?: ComponentType>; +} + +export function SimpleToolCallUI({ + contextItems, + icon: Icon, + toolCallState, + tool, +}: SimpleToolCallUIProps) { + const ctxItems = useMemo(() => { + return contextItems?.filter((ctxItem) => !ctxItem.hidden) ?? []; + }, [contextItems]); + + const [open, setOpen] = useState(false); + const [isHovered, setIsHovered] = useState(false); + + const [showingArgs, setShowingArgs] = useState(false); + + const args: [string, any][] = useMemo(() => { + return Object.entries(toolCallState.parsedArgs); + }, [toolCallState.parsedArgs]); + + return ( +
+
+
setOpen((prev) => !prev)} + onMouseEnter={() => setIsHovered(true)} + onMouseLeave={() => setIsHovered(false)} + data-testid="context-items-peek" + > +
+ {Icon && !isHovered && !open ? ( + + ) : ( + <> + + + + )} +
+ + + +
+
+ {args.length > 0 ? ( + + ) : null} +
+
+ +
+ {ctxItems.length ? ( + ctxItems.map((contextItem, idx) => ( + + )) + ) : ( +
+ No tool call output +
+ )} +
+
+ ); +} diff --git a/gui/src/pages/gui/ToolCallDiv/ToolCall.tsx b/gui/src/pages/gui/ToolCallDiv/ToolCall.tsx index 2d823b7f32..6f313236d3 100644 --- a/gui/src/pages/gui/ToolCallDiv/ToolCall.tsx +++ b/gui/src/pages/gui/ToolCallDiv/ToolCall.tsx @@ -1,102 +1,26 @@ -import { ChevronDownIcon, ChevronUpIcon } from "@heroicons/react/24/outline"; -import { Tool, ToolCallDelta, ToolCallState } from "core"; -import Mustache from "mustache"; -import { ReactNode, useMemo, useState } from "react"; -import { ToolTip } from "../../../components/gui/Tooltip"; -import { useAppSelector } from "../../../redux/hooks"; +import { Tool, ToolCallState } from "core"; +import { useMemo, useState } from "react"; +import { ArgsItems, ArgsToggleIcon } from "./ToolCallArgs"; +import { ToolCallStatusMessage } from "./ToolCallStatusMessage"; interface ToolCallDisplayProps { children: React.ReactNode; icon: React.ReactNode; - toolCall: ToolCallDelta; + tool: Tool | undefined; toolCallState: ToolCallState; } -export function getToolCallStatusMessage( - tool: Tool | undefined, - toolCallState: ToolCallState, -) { - if (!tool) return "Agent tool use"; - - const defaultToolDescription = ( - <> - {tool.displayTitle ?? tool.function.name} tool - - ); - - const futureMessage = tool.wouldLikeTo ? ( - Mustache.render(tool.wouldLikeTo, toolCallState.parsedArgs) - ) : ( - <> - use the {defaultToolDescription} - - ); - - let intro = ""; - let message: ReactNode = ""; - - if ( - toolCallState.status === "done" || - (tool.isInstant && toolCallState.status === "calling") - ) { - intro = ""; - message = tool.hasAlready ? ( - Mustache.render(tool.hasAlready, toolCallState.parsedArgs) - ) : ( - <> - used the {defaultToolDescription} - - ); - } else if (toolCallState.status === "generating") { - intro = "is generating output to"; - message = futureMessage; - } else if (toolCallState.status === "generated") { - intro = "wants to"; - message = futureMessage; - } else if (toolCallState.status === "calling") { - intro = "is"; - message = tool.isCurrently ? ( - Mustache.render(tool.isCurrently, toolCallState.parsedArgs) - ) : ( - <> - calling the {defaultToolDescription} - - ); - } else if ( - toolCallState.status === "canceled" || - toolCallState.status === "errored" - ) { - intro = "tried to"; - message = futureMessage; - } - return ( -
- Continue {intro} {message} -
- ); -} - -export function ToolCallDisplay(props: ToolCallDisplayProps) { - const [isExpanded, setIsExpanded] = useState(false); - const availableTools = useAppSelector((state) => state.config.config.tools); - - const tool = useMemo(() => { - return availableTools.find( - (tool) => props.toolCall.function?.name === tool.function.name, - ); - }, [availableTools, props.toolCall]); - - const statusMessage = useMemo(() => { - return getToolCallStatusMessage(tool, props.toolCallState); - }, [props.toolCallState, tool]); +export function ToolCallDisplay({ + tool, + toolCallState, + children, + icon, +}: ToolCallDisplayProps) { + const [argsExpanded, setArgsExpanded] = useState(false); const args: [string, any][] = useMemo(() => { - return Object.entries(props.toolCallState.parsedArgs); - }, [props.toolCallState.parsedArgs]); - - const argsTooltipId = useMemo(() => { - return "args-hover-" + props.toolCallState.toolCallId; - }, [props.toolCallState]); + return Object.entries(toolCallState.parsedArgs); + }, [toolCallState.parsedArgs]); return ( <> @@ -104,54 +28,32 @@ export function ToolCallDisplay(props: ToolCallDisplayProps) {
-
- {props.icon} +
+ {icon}
{tool?.faviconUrl && ( )}
- {statusMessage} +
{!!args.length ? ( -
setIsExpanded(!isExpanded)} - className="ml-2 cursor-pointer hover:opacity-80" - > - {isExpanded ? ( - - ) : ( - - )} -
+ ) : null} - - {isExpanded ? "Hide args" : "Show args"} -
- - {isExpanded && !!args.length && ( -
- {args.map(([key, value]) => ( -
- {key}: - {value.toString()} -
- ))} -
+ {argsExpanded && !!args.length && ( + )}
-
{props.children}
+
{children}
); diff --git a/gui/src/pages/gui/ToolCallDiv/ToolCallArgs.tsx b/gui/src/pages/gui/ToolCallDiv/ToolCallArgs.tsx new file mode 100644 index 0000000000..738be01f9a --- /dev/null +++ b/gui/src/pages/gui/ToolCallDiv/ToolCallArgs.tsx @@ -0,0 +1,64 @@ +import { CodeBracketIcon } from "@heroicons/react/24/outline"; +import { useMemo } from "react"; +import { ToolTip } from "../../../components/gui/Tooltip"; + +interface ArgsToggleIconProps { + isShowing: boolean; + setIsShowing: (val: boolean) => void; + toolCallId: string; +} + +export const ArgsToggleIcon = ({ + isShowing, + setIsShowing, + toolCallId, +}: ArgsToggleIconProps) => { + const argsTooltipId = useMemo(() => { + return "args-hover-" + toolCallId; + }, [toolCallId]); + + return ( + <> +
{ + e.stopPropagation(); + setIsShowing(!isShowing); + }} + className={`cursor-pointer select-none rounded-sm px-1 py-0.5 hover:bg-gray-400/40 hover:opacity-80 ${isShowing ? "bg-gray-400/40" : "bg-transparent"}`} + > + + {/* {`{}`} */} +
+ + {isShowing ? "Hide args" : "Show args"} + + + ); +}; + +interface ArgsItemsProps { + isShowing: boolean; + args: [string, string][]; +} + +export const ArgsItems = ({ args, isShowing }: ArgsItemsProps) => { + if (args.length === 0) { + return null; + } + + if (!isShowing) { + return null; + } + + return ( +
+ {args.map(([key, value]) => ( +
+ {key}: + {value.toString()} +
+ ))} +
+ ); +}; diff --git a/gui/src/pages/gui/ToolCallDiv/ToolCallStatusMessage.tsx b/gui/src/pages/gui/ToolCallDiv/ToolCallStatusMessage.tsx new file mode 100644 index 0000000000..e4e4c6e420 --- /dev/null +++ b/gui/src/pages/gui/ToolCallDiv/ToolCallStatusMessage.tsx @@ -0,0 +1,72 @@ +import { Tool, ToolCallState } from "core"; +import Mustache from "mustache"; +import { ReactNode } from "react"; + +interface ToolCallStatusMessageProps { + tool: Tool | undefined; + toolCallState: ToolCallState; +} + +export function ToolCallStatusMessage({ + tool, + toolCallState, +}: ToolCallStatusMessageProps) { + if (!tool) return "Agent tool use"; + + const defaultToolDescription = ( + <> + {tool.displayTitle ?? tool.function.name} tool + + ); + + const futureMessage = tool.wouldLikeTo ? ( + Mustache.render(tool.wouldLikeTo, toolCallState.parsedArgs) + ) : ( + <> + use the {defaultToolDescription} + + ); + + let intro = ""; + let message: ReactNode = ""; + + if ( + toolCallState.status === "done" || + (tool.isInstant && toolCallState.status === "calling") + ) { + intro = ""; + message = tool.hasAlready ? ( + Mustache.render(tool.hasAlready, toolCallState.parsedArgs) + ) : ( + <> + used the {defaultToolDescription} + + ); + } else if (toolCallState.status === "generating") { + intro = "is generating output to"; + message = futureMessage; + } else if (toolCallState.status === "generated") { + intro = "wants to"; + message = futureMessage; + } else if (toolCallState.status === "calling") { + intro = "is"; + message = tool.isCurrently ? ( + Mustache.render(tool.isCurrently, toolCallState.parsedArgs) + ) : ( + <> + calling the {defaultToolDescription} + + ); + } else if ( + toolCallState.status === "canceled" || + toolCallState.status === "errored" + ) { + intro = "tried to"; + message = futureMessage; + } + return ( +
+ Continue {intro} {message} +
+ ); +} diff --git a/gui/src/pages/gui/ToolCallDiv/ToolOutput.tsx b/gui/src/pages/gui/ToolCallDiv/ToolOutput.tsx deleted file mode 100644 index 069f839cae..0000000000 --- a/gui/src/pages/gui/ToolCallDiv/ToolOutput.tsx +++ /dev/null @@ -1,31 +0,0 @@ -import { ContextItemWithId } from "core"; -import { ComponentType } from "react"; -import ContextItemsPeek from "../../../components/mainInput/belowMainInput/ContextItemsPeek"; - -interface ToolOutputProps { - contextItems: ContextItemWithId[]; - toolCallId: string; - icon?: ComponentType; - title?: JSX.Element | string; -} - -function ToolOutput(props: ToolOutputProps) { - // Terminal has dedicated UI to show the output - if (props.contextItems.some((ci) => ci.name === "Terminal")) { - return null; - } - - return ( -
- -
- ); -} - -export default ToolOutput; diff --git a/gui/src/pages/gui/ToolCallDiv/index.tsx b/gui/src/pages/gui/ToolCallDiv/index.tsx index 5fd016d450..167ff191c1 100644 --- a/gui/src/pages/gui/ToolCallDiv/index.tsx +++ b/gui/src/pages/gui/ToolCallDiv/index.tsx @@ -8,6 +8,7 @@ import { FolderIcon, FolderOpenIcon, GlobeAltIcon, + ListBulletIcon, MagnifyingGlassIcon, MapIcon, XMarkIcon, @@ -24,8 +25,8 @@ import { vscButtonBackground } from "../../../components"; import Spinner from "../../../components/gui/Spinner"; import { useAppSelector } from "../../../redux/hooks"; import FunctionSpecificToolCallDiv from "./FunctionSpecificToolCallDiv"; -import { getToolCallStatusMessage, ToolCallDisplay } from "./ToolCall"; -import ToolOutput from "./ToolOutput"; +import { SimpleToolCallUI } from "./SimpleToolCallUI"; +import { ToolCallDisplay } from "./ToolCall"; interface ToolCallDivProps { toolCall: ToolCallDelta; @@ -44,24 +45,28 @@ const toolCallIcons: Record = { [BuiltInToolNames.ViewDiff]: CodeBracketIcon, [BuiltInToolNames.ViewRepoMap]: MapIcon, [BuiltInToolNames.ViewSubdirectory]: FolderOpenIcon, + [BuiltInToolNames.CreateRuleBlock]: ListBulletIcon, + // EditExistingFile = "builtin_edit_existing_file", + // CreateNewFile = "builtin_create_new_file", + // RunTerminalCommand = "builtin_run_terminal_command", }; -export function ToolCallDiv(props: ToolCallDivProps) { - function getIcon(state: ToolStatus) { - switch (state) { - case "generating": - case "calling": - return ; - case "generated": - return ; - case "done": - return ; - case "canceled": - case "errored": - return ; - } +function getStatusIcon(state: ToolStatus) { + switch (state) { + case "generating": + case "calling": + return ; + case "generated": + return ; + case "done": + return ; + case "canceled": + case "errored": + return ; } +} +export function ToolCallDiv(props: ToolCallDivProps) { const availableTools = useAppSelector((state) => state.config.config.tools); const tool = useMemo(() => { return availableTools.find( @@ -69,24 +74,20 @@ export function ToolCallDiv(props: ToolCallDivProps) { ); }, [availableTools, props.toolCall]); - const statusMessage = useMemo(() => { - return getToolCallStatusMessage(tool, props.toolCallState); - }, [props.toolCallState, tool]); - const icon = props.toolCall.function?.name && - toolCallIcons[props.toolCall.function?.name]; + toolCallIcons[props.toolCall.function.name]; - if (icon && props.toolCall.id) { + if (icon) { return ( -
- +
); @@ -94,8 +95,8 @@ export function ToolCallDiv(props: ToolCallDivProps) { return (