diff --git a/.github/workflows/pr_checks.yaml b/.github/workflows/pr_checks.yaml index 4d0a4039b5..098bc1b6cb 100644 --- a/.github/workflows/pr_checks.yaml +++ b/.github/workflows/pr_checks.yaml @@ -4,12 +4,10 @@ on: pull_request: branches: - main - - staging push: branches: - main - - staging jobs: install-root: diff --git a/core/index.d.ts b/core/index.d.ts index 696876f071..2d4a8ffc76 100644 --- a/core/index.d.ts +++ b/core/index.d.ts @@ -966,7 +966,10 @@ export interface ToolExtras { fetch: FetchFunction; tool: Tool; toolCallId?: string; - onPartialOutput?: (params: { toolCallId: string, contextItems: ContextItem[] }) => void; + onPartialOutput?: (params: { + toolCallId: string; + contextItems: ContextItem[]; + }) => void; } export interface Tool { @@ -983,6 +986,7 @@ export interface Tool { isCurrently?: string; hasAlready?: string; readonly: boolean; + isInstant?: boolean; uri?: string; faviconUrl?: string; group: string; diff --git a/core/package-lock.json b/core/package-lock.json index 0a0e8622b5..2536f0d29f 100644 --- a/core/package-lock.json +++ b/core/package-lock.json @@ -134,7 +134,7 @@ }, "../packages/config-yaml": { "name": "@continuedev/config-yaml", - "version": "1.0.78", + "version": "1.0.87", "license": "Apache-2.0", "dependencies": { "@continuedev/config-types": "^1.0.14", diff --git a/core/tools/definitions/createNewFile.ts b/core/tools/definitions/createNewFile.ts index 46db25cb9e..41bea0b91f 100644 --- a/core/tools/definitions/createNewFile.ts +++ b/core/tools/definitions/createNewFile.ts @@ -9,6 +9,7 @@ export const createNewFileTool: Tool = { hasAlready: "created a new file at {{{ filepath }}}", group: BUILT_IN_GROUP_NAME, readonly: false, + isInstant: true, function: { name: BuiltInToolNames.CreateNewFile, description: diff --git a/core/tools/definitions/createRuleBlock.ts b/core/tools/definitions/createRuleBlock.ts index bf5caa6dc8..5c7b69c64d 100644 --- a/core/tools/definitions/createRuleBlock.ts +++ b/core/tools/definitions/createRuleBlock.ts @@ -8,6 +8,7 @@ export const createRuleBlock: Tool = { isCurrently: 'creating a rule block for "{{{ rule_name }}}"', hasAlready: 'created a rule block for "{{{ rule_name }}}"', readonly: false, + isInstant: true, group: BUILT_IN_GROUP_NAME, function: { name: BuiltInToolNames.CreateRuleBlock, diff --git a/core/tools/definitions/globSearch.ts b/core/tools/definitions/globSearch.ts index bff2d454c5..3cbc43d52a 100644 --- a/core/tools/definitions/globSearch.ts +++ b/core/tools/definitions/globSearch.ts @@ -8,6 +8,7 @@ export const globSearchTool: Tool = { isCurrently: 'finding file matches for "{{{ pattern }}}"', hasAlready: 'retreived file matches for "{{{ pattern }}}"', readonly: true, + isInstant: true, group: BUILT_IN_GROUP_NAME, function: { name: BuiltInToolNames.FileGlobSearch, diff --git a/core/tools/definitions/grepSearch.ts b/core/tools/definitions/grepSearch.ts index 42ea92e64b..ca8a73a306 100644 --- a/core/tools/definitions/grepSearch.ts +++ b/core/tools/definitions/grepSearch.ts @@ -8,6 +8,7 @@ export const grepSearchTool: Tool = { isCurrently: 'getting search results for "{{{ query }}}"', hasAlready: 'retrieved search results for "{{{ query }}}"', readonly: true, + isInstant: true, group: BUILT_IN_GROUP_NAME, function: { name: BuiltInToolNames.GrepSearch, diff --git a/core/tools/definitions/lsTool.ts b/core/tools/definitions/lsTool.ts index 6b6e964983..a0bb96cbb0 100644 --- a/core/tools/definitions/lsTool.ts +++ b/core/tools/definitions/lsTool.ts @@ -9,6 +9,7 @@ export const lsTool: Tool = { isCurrently: "listing files and folders in {{{ dirPath }}}", hasAlready: "listed files and folders in {{{ dirPath }}}", readonly: true, + isInstant: true, group: BUILT_IN_GROUP_NAME, function: { name: BuiltInToolNames.LSTool, diff --git a/core/tools/definitions/readCurrentlyOpenFile.ts b/core/tools/definitions/readCurrentlyOpenFile.ts index eacaab70d4..9c15105a61 100644 --- a/core/tools/definitions/readCurrentlyOpenFile.ts +++ b/core/tools/definitions/readCurrentlyOpenFile.ts @@ -8,6 +8,7 @@ export const readCurrentlyOpenFileTool: Tool = { isCurrently: "reading the current file", hasAlready: "viewed the current file", readonly: true, + isInstant: true, group: BUILT_IN_GROUP_NAME, function: { name: BuiltInToolNames.ReadCurrentlyOpenFile, diff --git a/core/tools/definitions/readFile.ts b/core/tools/definitions/readFile.ts index ff5d58a31e..42737fe636 100644 --- a/core/tools/definitions/readFile.ts +++ b/core/tools/definitions/readFile.ts @@ -8,6 +8,7 @@ export const readFileTool: Tool = { isCurrently: "reading {{{ filepath }}}", hasAlready: "viewed {{{ filepath }}}", readonly: true, + isInstant: true, group: BUILT_IN_GROUP_NAME, function: { name: BuiltInToolNames.ReadFile, diff --git a/core/tools/definitions/viewDiff.ts b/core/tools/definitions/viewDiff.ts index e7ce716a87..d835986b0a 100644 --- a/core/tools/definitions/viewDiff.ts +++ b/core/tools/definitions/viewDiff.ts @@ -9,6 +9,7 @@ export const viewDiffTool: Tool = { isCurrently: "getting the git diff", hasAlready: "viewed the git diff", readonly: true, + isInstant: true, group: BUILT_IN_GROUP_NAME, function: { name: BuiltInToolNames.ViewDiff, diff --git a/core/tools/definitions/viewRepoMap.ts b/core/tools/definitions/viewRepoMap.ts index 21c8050a6c..dc76e83093 100644 --- a/core/tools/definitions/viewRepoMap.ts +++ b/core/tools/definitions/viewRepoMap.ts @@ -9,6 +9,7 @@ export const viewRepoMapTool: Tool = { isCurrently: "getting the repository map", hasAlready: "viewed the repository map", readonly: true, + isInstant: true, group: BUILT_IN_GROUP_NAME, function: { name: BuiltInToolNames.ViewRepoMap, diff --git a/core/tools/definitions/viewSubdirectory.ts b/core/tools/definitions/viewSubdirectory.ts index 2f9093c54a..41c1a904c0 100644 --- a/core/tools/definitions/viewSubdirectory.ts +++ b/core/tools/definitions/viewSubdirectory.ts @@ -9,6 +9,7 @@ export const viewSubdirectoryTool: Tool = { hasAlready: 'viewed a map of "{{{ directory_path }}}"', readonly: true, group: BUILT_IN_GROUP_NAME, + isInstant: true, function: { name: BuiltInToolNames.ViewSubdirectory, description: "View the contents of a subdirectory", diff --git a/extensions/vscode/e2e/selectors/GUI.selectors.ts b/extensions/vscode/e2e/selectors/GUI.selectors.ts index 098df7f9e5..45d2a23e6b 100644 --- a/extensions/vscode/e2e/selectors/GUI.selectors.ts +++ b/extensions/vscode/e2e/selectors/GUI.selectors.ts @@ -46,10 +46,7 @@ export class GUISelectors { } public static getToolCallStatusMessage(view: WebView) { - return SelectorUtils.getElementByDataTestId( - view, - "tool-call-status-message", - ); + return SelectorUtils.getElementByDataTestId(view, "toggle-div-title"); } public static getToolButton(view: WebView) { diff --git a/gui/package-lock.json b/gui/package-lock.json index c47bb8827e..c65e604e65 100644 --- a/gui/package-lock.json +++ b/gui/package-lock.json @@ -206,7 +206,7 @@ }, "../packages/config-yaml": { "name": "@continuedev/config-yaml", - "version": "1.0.78", + "version": "1.0.87", "license": "Apache-2.0", "dependencies": { "@continuedev/config-types": "^1.0.14", diff --git a/gui/src/components/ToggleDiv.tsx b/gui/src/components/ToggleDiv.tsx new file mode 100644 index 0000000000..82536fe105 --- /dev/null +++ b/gui/src/components/ToggleDiv.tsx @@ -0,0 +1,66 @@ +import { ChevronDownIcon, ChevronRightIcon } from "@heroicons/react/24/outline"; +import { ComponentType, useState } from "react"; +import { lightGray, vscBackground } from "."; + +interface ToggleProps { + children: React.ReactNode; + title: React.ReactNode; + icon?: ComponentType>; +} + +function ToggleDiv({ children, title, icon: Icon }: ToggleProps) { + const [open, setOpen] = useState(false); + const [isHovered, setIsHovered] = useState(false); + + return ( +
+
setOpen((prev) => !prev)} + onMouseEnter={() => setIsHovered(true)} + onMouseLeave={() => setIsHovered(false)} + data-testid="context-items-peek" + > +
+ {Icon && !isHovered && !open ? ( + + ) : ( + <> + + + + )} +
+ + {title} + +
+ +
+ {children} +
+
+ ); +} + +export default ToggleDiv; diff --git a/gui/src/components/mainInput/InputToolbar.tsx b/gui/src/components/mainInput/InputToolbar.tsx index 08c5a910f9..4172671885 100644 --- a/gui/src/components/mainInput/InputToolbar.tsx +++ b/gui/src/components/mainInput/InputToolbar.tsx @@ -3,14 +3,7 @@ import { InputModifiers } from "core"; import { modelSupportsImages, modelSupportsTools } from "core/llm/autodetect"; import { useRef } from "react"; import styled from "styled-components"; -import { - defaultBorderRadius, - lightGray, - vscButtonBackground, - vscButtonForeground, - vscForeground, - vscInputBackground, -} from ".."; +import { vscInputBackground } from ".."; import { useAppDispatch, useAppSelector } from "../../redux/hooks"; import { selectUseActiveFile } from "../../redux/selectors"; import { selectCurrentToolCall } from "../../redux/selectors/selectCurrentToolCall"; @@ -31,6 +24,7 @@ import { ToolTip } from "../gui/Tooltip"; import ModelSelect from "../modelSelection/ModelSelect"; import ModeSelect from "../modelSelection/ModeSelect"; import { useFontSize } from "../ui/font"; +import { EnterButton } from "./InputToolbar/EnterButton"; import HoverItem from "./InputToolbar/HoverItem"; const StyledDiv = styled.div<{ isHidden?: boolean }>` @@ -50,25 +44,6 @@ const StyledDiv = styled.div<{ isHidden?: boolean }>` } `; -const EnterButton = styled.button<{ isPrimary?: boolean }>` - all: unset; - padding: 2px 4px; - display: flex; - align-items: center; - background-color: ${(props) => - !props.disabled && props.isPrimary - ? vscButtonBackground - : lightGray + "33"}; - border-radius: ${defaultBorderRadius}; - color: ${(props) => - !props.disabled && props.isPrimary ? vscButtonForeground : vscForeground}; - cursor: pointer; - - :disabled { - cursor: wait; - } -`; - export interface ToolbarOptions { hideUseCodebase?: boolean; hideImageUpload?: boolean; diff --git a/gui/src/components/mainInput/InputToolbar/EnterButton.tsx b/gui/src/components/mainInput/InputToolbar/EnterButton.tsx new file mode 100644 index 0000000000..1eb5871d22 --- /dev/null +++ b/gui/src/components/mainInput/InputToolbar/EnterButton.tsx @@ -0,0 +1,29 @@ +import styled from "styled-components"; +import { + defaultBorderRadius, + lightGray, + vscButtonBackground, + vscButtonForeground, + vscForeground, +} from "../.."; +import { fontSize } from "../../../util"; + +export const EnterButton = styled.button<{ isPrimary?: boolean }>` + all: unset; + font-size: ${fontSize(-3)}; + padding: 2px 4px; + display: flex; + align-items: center; + background-color: ${(props) => + !props.disabled && props.isPrimary + ? vscButtonBackground + : lightGray + "33"}; + border-radius: ${defaultBorderRadius}; + color: ${(props) => + !props.disabled && props.isPrimary ? vscButtonForeground : vscForeground}; + cursor: pointer; + + :disabled { + cursor: wait; + } +`; diff --git a/gui/src/components/mainInput/Lump/LumpToolbar.tsx b/gui/src/components/mainInput/Lump/LumpToolbar.tsx index 2cac02cbb2..dd91d4e667 100644 --- a/gui/src/components/mainInput/Lump/LumpToolbar.tsx +++ b/gui/src/components/mainInput/Lump/LumpToolbar.tsx @@ -1,10 +1,15 @@ -import { useContext } from "react"; +import { useContext, useEffect } from "react"; +import { useSelector } from "react-redux"; import styled from "styled-components"; import { AnimatedEllipsis } from "../.."; import { IdeMessengerContext } from "../../../context/IdeMessenger"; import { useAppDispatch, useAppSelector } from "../../../redux/hooks"; +import { selectCurrentToolCall } from "../../../redux/selectors/selectCurrentToolCall"; +import { callTool } from "../../../redux/thunks/callTool"; import { cancelStream } from "../../../redux/thunks/cancelStream"; +import { cancelTool } from "../../../redux/thunks/cancelTool"; import { getFontSize, getMetaKeyLabel } from "../../../util"; +import { EnterButton } from "../InputToolbar/EnterButton"; import { BlockSettingsTopToolbar } from "./BlockSettingsTopToolbar"; const Container = styled.div` @@ -36,6 +41,31 @@ export function LumpToolbar() { const ttsActive = useAppSelector((state) => state.ui.ttsActive); const isStreaming = useAppSelector((state) => state.session.isStreaming); + const toolCallState = useSelector(selectCurrentToolCall); + + const handleKeyDown = (event: KeyboardEvent) => { + if (toolCallState?.status === "generated") { + const metaKey = event.metaKey || event.ctrlKey; + + if (metaKey && event.key === "Enter") { + event.preventDefault(); + event.stopPropagation(); + dispatch(callTool()); + } else if (metaKey && event.key === "Backspace") { + event.preventDefault(); + event.stopPropagation(); + dispatch(cancelTool()); + } + } + }; + + useEffect(() => { + document.addEventListener("keydown", handleKeyDown); + return () => { + document.removeEventListener("keydown", handleKeyDown); + }; + }, [toolCallState]); + if (ttsActive) { return ( @@ -68,5 +98,31 @@ export function LumpToolbar() { ); } + if (toolCallState?.status === "generated") { + return ( + + + +
+ dispatch(cancelTool())} + data-testid="reject-tool-call-button" + > + {getMetaKeyLabel()} ⌫ Cancel + + dispatch(callTool())} + data-testid="accept-tool-call-button" + > + {getMetaKeyLabel()} ⏎ Continue + +
+
+ ); + } + return ; } diff --git a/gui/src/components/mainInput/belowMainInput/ContextItemsPeek.tsx b/gui/src/components/mainInput/belowMainInput/ContextItemsPeek.tsx index bb153f62ac..5840240c2e 100644 --- a/gui/src/components/mainInput/belowMainInput/ContextItemsPeek.tsx +++ b/gui/src/components/mainInput/belowMainInput/ContextItemsPeek.tsx @@ -1,24 +1,24 @@ -import { - ArrowTopRightOnSquareIcon, - ChevronDownIcon, - ChevronRightIcon, -} from "@heroicons/react/24/outline"; +import { ArrowTopRightOnSquareIcon } from "@heroicons/react/24/outline"; import { ContextItemWithId } from "core"; import { ctxItemToRifWithContents } from "core/commands/util"; import { getUriPathBasename } from "core/util/uri"; -import { useContext, useMemo, useState } from "react"; -import { AnimatedEllipsis, lightGray, vscBackground } from "../.."; +import { ComponentType, useContext, useMemo } from "react"; +import { AnimatedEllipsis } from "../.."; import { IdeMessengerContext } from "../../../context/IdeMessenger"; import { useAppSelector } from "../../../redux/hooks"; import { selectIsGatheringContext } from "../../../redux/slices/sessionSlice"; import FileIcon from "../../FileIcon"; import SafeImg from "../../SafeImg"; +import ToggleDiv from "../../ToggleDiv"; import { getIconFromDropdownItem } from "../AtMentionDropdown"; import { NAMED_ICONS } from "../icons"; interface ContextItemsPeekProps { contextItems?: ContextItemWithId[]; isCurrentContextPeek: boolean; + icon?: ComponentType>; + title?: JSX.Element | string; + showWhenNoResults?: boolean; } interface ContextItemsPeekItemProps { @@ -153,9 +153,10 @@ function ContextItemsPeekItem({ contextItem }: ContextItemsPeekItemProps) { function ContextItemsPeek({ contextItems, isCurrentContextPeek, + icon, + title, + showWhenNoResults, }: ContextItemsPeekProps) { - const [open, setOpen] = useState(false); - const ctxItems = useMemo(() => { return contextItems?.filter((ctxItem) => !ctxItem.hidden) ?? []; }, [contextItems]); @@ -164,59 +165,37 @@ function ContextItemsPeek({ const indicateIsGathering = isCurrentContextPeek && isGatheringContext; - if ((!ctxItems || ctxItems.length === 0) && !indicateIsGathering) { + if ( + !showWhenNoResults && + (!ctxItems || ctxItems.length === 0) && + !indicateIsGathering + ) { return null; } return ( -
+ Gathering context + + + ) : ( + `${ctxItems.length} context ${ctxItems.length > 1 ? "items" : "item"}` + )) + } > -
setOpen((prev) => !prev)} - data-testid="context-items-peek" - > -
- - -
- - {isGatheringContext ? ( - <> - Gathering context - - - ) : ( - `${ctxItems.length} context ${ - ctxItems.length > 1 ? "items" : "item" - }` - )} - -
- -
- {ctxItems && - ctxItems.map((contextItem, idx) => ( - - ))} -
-
+ {ctxItems.length ? ( + ctxItems.map((contextItem, idx) => ( + + )) + ) : ( +
No results
+ )} + ); } diff --git a/gui/src/pages/gui/Chat.tsx b/gui/src/pages/gui/Chat.tsx index 11017177bd..8a76cdbf9f 100644 --- a/gui/src/pages/gui/Chat.tsx +++ b/gui/src/pages/gui/Chat.tsx @@ -37,7 +37,7 @@ import { newSession, selectIsInEditMode, selectIsSingleRangeEditOrInsertion, - updateToolCallOutput + updateToolCallOutput, } from "../../redux/slices/sessionSlice"; import { setDialogEntryOn, @@ -58,8 +58,6 @@ import { getLocalStorage, setLocalStorage } from "../../util/localStorage"; import { EmptyChatBody } from "./EmptyChatBody"; import { ExploreDialogWatcher } from "./ExploreDialogWatcher"; import { ToolCallDiv } from "./ToolCallDiv"; -import { ToolCallButtons } from "./ToolCallDiv/ToolCallButtonsDiv"; -import ToolOutput from "./ToolCallDiv/ToolOutput"; import { useAutoScroll } from "./useAutoScroll"; const StepsDiv = styled.div` @@ -277,13 +275,13 @@ export function Chat() { useWebviewListener( "toolCallPartialOutput", async (data) => { - // Update tool call output in Redux store - dispatch( - updateToolCallOutput({ - toolCallId: data.toolCallId, - contextItems: data.contextItems, - }), - ); + // Update tool call output in Redux store + dispatch( + updateToolCallOutput({ + toolCallId: data.toolCallId, + contextItems: data.contextItems, + }), + ); }, [dispatch], ); @@ -342,12 +340,11 @@ export function Chat() { inputId={item.message.id} /> - ) : item.message.role === "tool" ? ( - - ) : item.message.role === "assistant" && + ) : item.message.role === "tool" ? null : // + item.message.role === "assistant" && item.message.toolCalls && item.toolCallState ? (
@@ -357,6 +354,7 @@ export function Chat() {
); @@ -410,8 +408,6 @@ export function Chat() { ))}
- {toolCallState?.status === "generated" && } - {isInEditMode && history.length === 0 && } {isInEditMode && history.length > 0 ? null : ( diff --git a/gui/src/pages/gui/ToolCallDiv/ToolCall.tsx b/gui/src/pages/gui/ToolCallDiv/ToolCall.tsx index c8a8520b05..0abee8f3f0 100644 --- a/gui/src/pages/gui/ToolCallDiv/ToolCall.tsx +++ b/gui/src/pages/gui/ToolCallDiv/ToolCall.tsx @@ -1,5 +1,5 @@ import { ChevronDownIcon, ChevronUpIcon } from "@heroicons/react/24/outline"; -import { ToolCallDelta, ToolCallState } from "core"; +import { Tool, ToolCallDelta, ToolCallState } from "core"; import Mustache from "mustache"; import { ReactNode, useMemo, useState } from "react"; import { ToolTip } from "../../../components/gui/Tooltip"; @@ -12,69 +12,79 @@ interface ToolCallDisplayProps { toolCallState: ToolCallState; } -export function ToolCallDisplay(props: ToolCallDisplayProps) { - const [isExpanded, setIsExpanded] = useState(false); - const availableTools = useAppSelector((state) => state.config.config.tools); +export function getToolCallStatusMessage( + tool: Tool | undefined, + toolCallState: ToolCallState, +) { + if (!tool) return "Agent tool use"; - const tool = useMemo(() => { - return availableTools.find( - (tool) => props.toolCall.function?.name === tool.function.name, - ); - }, [availableTools, props.toolCall]); + const defaultToolDescription = ( + <> + {tool.displayTitle ?? tool.function.name} tool + + ); - const statusMessage = useMemo(() => { - if (!tool) return "Agent tool use"; + const futureMessage = tool.wouldLikeTo ? ( + Mustache.render(tool.wouldLikeTo, toolCallState.parsedArgs) + ) : ( + <> + use the {defaultToolDescription} + + ); + + let intro = ""; + let message: ReactNode = ""; - const defaultToolDescription = ( + if ( + toolCallState.status === "done" || + (tool.isInstant && toolCallState.status === "calling") + ) { + intro = ""; + message = tool.hasAlready ? ( + Mustache.render(tool.hasAlready, toolCallState.parsedArgs) + ) : ( <> - {tool.displayTitle ?? tool.function.name} tool + used the {defaultToolDescription} ); - - const futureMessage = tool.wouldLikeTo ? ( - Mustache.render(tool.wouldLikeTo, props.toolCallState.parsedArgs) + } 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) ) : ( <> - use the {defaultToolDescription} + calling the {defaultToolDescription} ); + } else if (toolCallState.status === "canceled") { + intro = "tried to"; + message = futureMessage; + } + return ( +
+ Continue {intro} {message} +
+ ); +} - let intro = ""; - let message: ReactNode = ""; +export function ToolCallDisplay(props: ToolCallDisplayProps) { + const [isExpanded, setIsExpanded] = useState(false); + const availableTools = useAppSelector((state) => state.config.config.tools); - if (props.toolCallState.status === "generating") { - intro = "is generating output to"; - message = futureMessage; - } else if (props.toolCallState.status === "generated") { - intro = "wants to"; - message = futureMessage; - } else if (props.toolCallState.status === "calling") { - intro = "is"; - message = tool.isCurrently ? ( - Mustache.render(tool.isCurrently, props.toolCallState.parsedArgs) - ) : ( - <> - calling the {defaultToolDescription} - - ); - } else if (props.toolCallState.status === "done") { - intro = ""; - message = tool.hasAlready ? ( - Mustache.render(tool.hasAlready, props.toolCallState.parsedArgs) - ) : ( - <> - used the {defaultToolDescription} - - ); - } else if (props.toolCallState.status === "canceled") { - intro = "tried to"; - message = futureMessage; - } - return ( -
- Continue {intro} {message} -
+ 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]); const args: [string, any][] = useMemo(() => { diff --git a/gui/src/pages/gui/ToolCallDiv/ToolCallButtonsDiv.tsx b/gui/src/pages/gui/ToolCallDiv/ToolCallButtonsDiv.tsx deleted file mode 100644 index 3d770706ba..0000000000 --- a/gui/src/pages/gui/ToolCallDiv/ToolCallButtonsDiv.tsx +++ /dev/null @@ -1,97 +0,0 @@ -import { useSelector } from "react-redux"; -import styled from "styled-components"; -import { - defaultBorderRadius, - lightGray, - vscButtonBackground, - vscButtonForeground, -} from "../../../components"; -import Spinner from "../../../components/gui/Spinner"; -import { useAppDispatch } from "../../../redux/hooks"; -import { selectCurrentToolCall } from "../../../redux/selectors/selectCurrentToolCall"; -import { callTool } from "../../../redux/thunks/callTool"; -import { cancelTool } from "../../../redux/thunks/cancelTool"; - -const ButtonContainer = styled.div` - display: flex; - gap: 8px; - margin-top: 12px; - margin: 8px; -`; - -const Button = styled.button` - padding: 5px; - border-radius: ${defaultBorderRadius}; - flex: 1; - - &:hover { - cursor: pointer; - opacity: 0.8; - } -`; - -const AcceptButton = styled(Button)` - color: ${vscButtonForeground}; - border: none; - background-color: ${vscButtonBackground}; - color: ${vscButtonForeground}; - - &:hover { - cursor: pointer; - } -`; - -const RejectButton = styled(Button)` - color: ${lightGray}; - border: 1px solid ${lightGray}; - background-color: transparent; -`; - -interface ToolCallButtonsProps {} - -export function ToolCallButtons(props: ToolCallButtonsProps) { - const dispatch = useAppDispatch(); - const toolCallState = useSelector(selectCurrentToolCall); - - if (!toolCallState) { - return null; - } - - return ( - <> - - {toolCallState.status === "generating" ? ( -
- Thinking... -
- ) : toolCallState.status === "generated" ? ( - <> - dispatch(cancelTool())} - data-testid="reject-tool-call-button" - > - Cancel - - dispatch(callTool())} - data-testid="accept-tool-call-button" - > - Continue - - - ) : toolCallState.status === "calling" ? ( -
- Loading... - -
- ) : null} -
- - ); -} diff --git a/gui/src/pages/gui/ToolCallDiv/ToolOutput.tsx b/gui/src/pages/gui/ToolCallDiv/ToolOutput.tsx index dcf0cdc475..069f839cae 100644 --- a/gui/src/pages/gui/ToolCallDiv/ToolOutput.tsx +++ b/gui/src/pages/gui/ToolCallDiv/ToolOutput.tsx @@ -1,9 +1,12 @@ 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) { @@ -15,6 +18,9 @@ function ToolOutput(props: ToolOutputProps) { return (
diff --git a/gui/src/pages/gui/ToolCallDiv/index.tsx b/gui/src/pages/gui/ToolCallDiv/index.tsx index 22c5f9de89..06cf745151 100644 --- a/gui/src/pages/gui/ToolCallDiv/index.tsx +++ b/gui/src/pages/gui/ToolCallDiv/index.tsx @@ -1,19 +1,50 @@ import { ArrowRightIcon, CheckIcon, + CodeBracketIcon, + CommandLineIcon, + DocumentIcon, + DocumentTextIcon, + FolderIcon, + FolderOpenIcon, + GlobeAltIcon, + MagnifyingGlassIcon, + MapIcon, XMarkIcon, } from "@heroicons/react/24/outline"; -import { ToolCallDelta, ToolCallState, ToolStatus } from "core"; +import { + ContextItemWithId, + ToolCallDelta, + ToolCallState, + ToolStatus, +} from "core"; +import { BuiltInToolNames } from "core/tools/builtIn"; +import { ComponentType, useMemo } from "react"; import { vscButtonBackground } from "../../../components"; import Spinner from "../../../components/gui/Spinner"; +import { useAppSelector } from "../../../redux/hooks"; import FunctionSpecificToolCallDiv from "./FunctionSpecificToolCallDiv"; -import { ToolCallDisplay } from "./ToolCall"; +import { getToolCallStatusMessage, ToolCallDisplay } from "./ToolCall"; +import ToolOutput from "./ToolOutput"; interface ToolCallDivProps { toolCall: ToolCallDelta; toolCallState: ToolCallState; + output?: ContextItemWithId[]; } +const toolCallIcons: Record = { + [BuiltInToolNames.FileGlobSearch]: MagnifyingGlassIcon, + [BuiltInToolNames.GrepSearch]: CommandLineIcon, + [BuiltInToolNames.LSTool]: FolderIcon, + [BuiltInToolNames.ReadCurrentlyOpenFile]: DocumentTextIcon, + [BuiltInToolNames.ReadFile]: DocumentIcon, + [BuiltInToolNames.SearchWeb]: GlobeAltIcon, + [BuiltInToolNames.ViewDiff]: CodeBracketIcon, + [BuiltInToolNames.ViewRepoMap]: MapIcon, + [BuiltInToolNames.ViewSubdirectory]: FolderOpenIcon, +}; + export function ToolCallDiv(props: ToolCallDivProps) { function getIcon(state: ToolStatus) { switch (state) { @@ -29,6 +60,35 @@ export function ToolCallDiv(props: ToolCallDivProps) { } } + 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]); + + const icon = + props.toolCall.function?.name && + toolCallIcons[props.toolCall.function?.name]; + if (icon && props.toolCall.id) { + return ( +
+ +
+ ); + } + return (