Skip to content

Tool UI Improvements #5321

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 7 commits into from
Apr 28, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion gui/src/components/mainInput/ContinueInputBox.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,14 +18,15 @@ interface ContextItemsPeekProps {
isCurrentContextPeek: boolean;
icon?: ComponentType<React.SVGProps<SVGSVGElement>>;
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";

Expand Down Expand Up @@ -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) ?? [];
Expand All @@ -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;
}

Expand Down Expand Up @@ -198,5 +194,3 @@ function ContextItemsPeek({
</ToggleDiv>
);
}

export default ContextItemsPeek;
97 changes: 97 additions & 0 deletions gui/src/pages/gui/ToolCallDiv/SimpleToolCallUI.tsx
Original file line number Diff line number Diff line change
@@ -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<React.SVGProps<SVGSVGElement>>;
}

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 (
<div className={`flex flex-1 flex-col px-2 pt-2`}>
<div className="flex flex-row justify-between">
<div
className="flex cursor-pointer items-center justify-start text-xs text-gray-300"
onClick={() => setOpen((prev) => !prev)}
onMouseEnter={() => setIsHovered(true)}
onMouseLeave={() => setIsHovered(false)}
data-testid="context-items-peek"
>
<div className="relative mr-1 h-4 w-4">
{Icon && !isHovered && !open ? (
<Icon className={`absolute h-4 w-4 text-gray-400`} />
) : (
<>
<ChevronRightIcon
className={`absolute h-4 w-4 text-gray-400 transition-all duration-200 ease-in-out ${
open ? "rotate-90 opacity-0" : "rotate-0 opacity-100"
}`}
/>
<ChevronDownIcon
className={`absolute h-4 w-4 text-gray-400 transition-all duration-200 ease-in-out ${
open ? "rotate-0 opacity-100" : "-rotate-90 opacity-0"
}`}
/>
</>
)}
</div>
<span
className="ml-1 text-xs text-gray-400 transition-colors duration-200"
data-testid="toggle-div-title"
>
<ToolCallStatusMessage tool={tool} toolCallState={toolCallState} />
</span>
</div>
<div>
{args.length > 0 ? (
<ArgsToggleIcon
isShowing={showingArgs}
setIsShowing={setShowingArgs}
toolCallId={toolCallState.toolCallId}
/>
) : null}
</div>
</div>
<ArgsItems args={args} isShowing={showingArgs} />
<div
className={`mt-2 overflow-y-auto transition-all duration-300 ease-in-out ${
open ? "max-h-[50vh] opacity-100" : "max-h-0 opacity-0"
}`}
>
{ctxItems.length ? (
ctxItems.map((contextItem, idx) => (
<ContextItemsPeekItem key={idx} contextItem={contextItem} />
))
) : (
<div className="pl-2 text-xs italic text-gray-400">
No tool call output
</div>
)}
</div>
</div>
);
}
154 changes: 28 additions & 126 deletions gui/src/pages/gui/ToolCallDiv/ToolCall.tsx
Original file line number Diff line number Diff line change
@@ -1,157 +1,59 @@
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 = (
<>
<code>{tool.displayTitle ?? tool.function.name}</code> <span>tool</span>
</>
);

const futureMessage = tool.wouldLikeTo ? (
Mustache.render(tool.wouldLikeTo, toolCallState.parsedArgs)
) : (
<>
<span>use the</span> {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)
) : (
<>
<span>used the</span> {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)
) : (
<>
<span>calling the</span> {defaultToolDescription}
</>
);
} else if (
toolCallState.status === "canceled" ||
toolCallState.status === "errored"
) {
intro = "tried to";
message = futureMessage;
}
return (
<div className="block">
<span>Continue</span> {intro} {message}
</div>
);
}

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 (
<>
<div className="relative flex flex-col justify-center p-4 pb-0">
<div className="mb-4 flex flex-col">
<div className="flex flex-row items-center justify-between gap-3">
<div className="flex flex-row gap-2">
<div
style={{
width: `16px`,
height: `16px`,
fontWeight: "bolder",
marginTop: "1px",
flexShrink: 0,
}}
>
{props.icon}
<div className="mt-[1px] h-4 w-4 flex-shrink-0 font-semibold">
{icon}
</div>
{tool?.faviconUrl && (
<img src={tool.faviconUrl} className="h-4 w-4 rounded-sm" />
)}
<div className="flex" data-testid="tool-call-status-message">
{statusMessage}
<ToolCallStatusMessage
tool={tool}
toolCallState={toolCallState}
/>
</div>
</div>
{!!args.length ? (
<div
data-tooltip-id={argsTooltipId}
onClick={() => setIsExpanded(!isExpanded)}
className="ml-2 cursor-pointer hover:opacity-80"
>
{isExpanded ? (
<ChevronUpIcon className="h-4 w-4" />
) : (
<ChevronDownIcon className="h-4 w-4" />
)}
</div>
<ArgsToggleIcon
isShowing={argsExpanded}
setIsShowing={setArgsExpanded}
toolCallId={toolCallState.toolCallId}
/>
) : null}
<ToolTip id={argsTooltipId}>
{isExpanded ? "Hide args" : "Show args"}
</ToolTip>
</div>

{isExpanded && !!args.length && (
<div className="ml-7 mt-1">
{args.map(([key, value]) => (
<div key={key} className="flex gap-2 py-0.5">
<span className="text-lightgray">{key}:</span>
<code className="line-clamp-1">{value.toString()}</code>
</div>
))}
</div>
{argsExpanded && !!args.length && (
<ArgsItems args={args} isShowing={argsExpanded} />
)}
</div>
<div>{props.children}</div>
<div>{children}</div>
</div>
</>
);
Expand Down
Loading
Loading