Skip to content

Commit 5d9128e

Browse files
authored
Merge pull request #5321 from continuedev/dallin/tool-ui-v2
Tool UI Improvements
2 parents 35c2d1b + fcc3427 commit 5d9128e

File tree

8 files changed

+296
-197
lines changed

8 files changed

+296
-197
lines changed

gui/src/components/mainInput/ContinueInputBox.tsx

+1-1
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import styled, { keyframes } from "styled-components";
55
import { defaultBorderRadius, vscBackground } from "..";
66
import { useAppSelector } from "../../redux/hooks";
77
import { selectSlashCommandComboBoxInputs } from "../../redux/selectors";
8-
import ContextItemsPeek from "./belowMainInput/ContextItemsPeek";
8+
import { ContextItemsPeek } from "./belowMainInput/ContextItemsPeek";
99
import { ToolbarOptions } from "./InputToolbar";
1010
import { Lump } from "./Lump";
1111
import { TipTapEditor } from "./TipTapEditor";

gui/src/components/mainInput/belowMainInput/ContextItemsPeek.tsx

+5-11
Original file line numberDiff line numberDiff line change
@@ -18,14 +18,15 @@ interface ContextItemsPeekProps {
1818
isCurrentContextPeek: boolean;
1919
icon?: ComponentType<React.SVGProps<SVGSVGElement>>;
2020
title?: JSX.Element | string;
21-
showWhenNoResults?: boolean;
2221
}
2322

2423
interface ContextItemsPeekItemProps {
2524
contextItem: ContextItemWithId;
2625
}
2726

28-
function ContextItemsPeekItem({ contextItem }: ContextItemsPeekItemProps) {
27+
export function ContextItemsPeekItem({
28+
contextItem,
29+
}: ContextItemsPeekItemProps) {
2930
const ideMessenger = useContext(IdeMessengerContext);
3031
const isUrl = contextItem.uri?.type === "url";
3132

@@ -150,12 +151,11 @@ function ContextItemsPeekItem({ contextItem }: ContextItemsPeekItemProps) {
150151
);
151152
}
152153

153-
function ContextItemsPeek({
154+
export function ContextItemsPeek({
154155
contextItems,
155156
isCurrentContextPeek,
156157
icon,
157158
title,
158-
showWhenNoResults,
159159
}: ContextItemsPeekProps) {
160160
const ctxItems = useMemo(() => {
161161
return contextItems?.filter((ctxItem) => !ctxItem.hidden) ?? [];
@@ -165,11 +165,7 @@ function ContextItemsPeek({
165165

166166
const indicateIsGathering = isCurrentContextPeek && isGatheringContext;
167167

168-
if (
169-
!showWhenNoResults &&
170-
(!ctxItems || ctxItems.length === 0) &&
171-
!indicateIsGathering
172-
) {
168+
if ((!ctxItems || ctxItems.length === 0) && !indicateIsGathering) {
173169
return null;
174170
}
175171

@@ -198,5 +194,3 @@ function ContextItemsPeek({
198194
</ToggleDiv>
199195
);
200196
}
201-
202-
export default ContextItemsPeek;
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
import { ChevronDownIcon, ChevronRightIcon } from "@heroicons/react/24/outline";
2+
import { ContextItemWithId, Tool, ToolCallState } from "core";
3+
import { ComponentType, useMemo, useState } from "react";
4+
import { ContextItemsPeekItem } from "../../../components/mainInput/belowMainInput/ContextItemsPeek";
5+
import { ArgsItems, ArgsToggleIcon } from "./ToolCallArgs";
6+
import { ToolCallStatusMessage } from "./ToolCallStatusMessage";
7+
8+
interface SimpleToolCallUIProps {
9+
toolCallState: ToolCallState;
10+
tool: Tool | undefined;
11+
contextItems: ContextItemWithId[];
12+
icon?: ComponentType<React.SVGProps<SVGSVGElement>>;
13+
}
14+
15+
export function SimpleToolCallUI({
16+
contextItems,
17+
icon: Icon,
18+
toolCallState,
19+
tool,
20+
}: SimpleToolCallUIProps) {
21+
const ctxItems = useMemo(() => {
22+
return contextItems?.filter((ctxItem) => !ctxItem.hidden) ?? [];
23+
}, [contextItems]);
24+
25+
const [open, setOpen] = useState(false);
26+
const [isHovered, setIsHovered] = useState(false);
27+
28+
const [showingArgs, setShowingArgs] = useState(false);
29+
30+
const args: [string, any][] = useMemo(() => {
31+
return Object.entries(toolCallState.parsedArgs);
32+
}, [toolCallState.parsedArgs]);
33+
34+
return (
35+
<div className={`flex flex-1 flex-col px-2 pt-2`}>
36+
<div className="flex flex-row justify-between">
37+
<div
38+
className="flex cursor-pointer items-center justify-start text-xs text-gray-300"
39+
onClick={() => setOpen((prev) => !prev)}
40+
onMouseEnter={() => setIsHovered(true)}
41+
onMouseLeave={() => setIsHovered(false)}
42+
data-testid="context-items-peek"
43+
>
44+
<div className="relative mr-1 h-4 w-4">
45+
{Icon && !isHovered && !open ? (
46+
<Icon className={`absolute h-4 w-4 text-gray-400`} />
47+
) : (
48+
<>
49+
<ChevronRightIcon
50+
className={`absolute h-4 w-4 text-gray-400 transition-all duration-200 ease-in-out ${
51+
open ? "rotate-90 opacity-0" : "rotate-0 opacity-100"
52+
}`}
53+
/>
54+
<ChevronDownIcon
55+
className={`absolute h-4 w-4 text-gray-400 transition-all duration-200 ease-in-out ${
56+
open ? "rotate-0 opacity-100" : "-rotate-90 opacity-0"
57+
}`}
58+
/>
59+
</>
60+
)}
61+
</div>
62+
<span
63+
className="ml-1 text-xs text-gray-400 transition-colors duration-200"
64+
data-testid="toggle-div-title"
65+
>
66+
<ToolCallStatusMessage tool={tool} toolCallState={toolCallState} />
67+
</span>
68+
</div>
69+
<div>
70+
{args.length > 0 ? (
71+
<ArgsToggleIcon
72+
isShowing={showingArgs}
73+
setIsShowing={setShowingArgs}
74+
toolCallId={toolCallState.toolCallId}
75+
/>
76+
) : null}
77+
</div>
78+
</div>
79+
<ArgsItems args={args} isShowing={showingArgs} />
80+
<div
81+
className={`mt-2 overflow-y-auto transition-all duration-300 ease-in-out ${
82+
open ? "max-h-[50vh] opacity-100" : "max-h-0 opacity-0"
83+
}`}
84+
>
85+
{ctxItems.length ? (
86+
ctxItems.map((contextItem, idx) => (
87+
<ContextItemsPeekItem key={idx} contextItem={contextItem} />
88+
))
89+
) : (
90+
<div className="pl-2 text-xs italic text-gray-400">
91+
No tool call output
92+
</div>
93+
)}
94+
</div>
95+
</div>
96+
);
97+
}

gui/src/pages/gui/ToolCallDiv/ToolCall.tsx

+28-126
Original file line numberDiff line numberDiff line change
@@ -1,157 +1,59 @@
1-
import { ChevronDownIcon, ChevronUpIcon } from "@heroicons/react/24/outline";
2-
import { Tool, ToolCallDelta, ToolCallState } from "core";
3-
import Mustache from "mustache";
4-
import { ReactNode, useMemo, useState } from "react";
5-
import { ToolTip } from "../../../components/gui/Tooltip";
6-
import { useAppSelector } from "../../../redux/hooks";
1+
import { Tool, ToolCallState } from "core";
2+
import { useMemo, useState } from "react";
3+
import { ArgsItems, ArgsToggleIcon } from "./ToolCallArgs";
4+
import { ToolCallStatusMessage } from "./ToolCallStatusMessage";
75

86
interface ToolCallDisplayProps {
97
children: React.ReactNode;
108
icon: React.ReactNode;
11-
toolCall: ToolCallDelta;
9+
tool: Tool | undefined;
1210
toolCallState: ToolCallState;
1311
}
1412

15-
export function getToolCallStatusMessage(
16-
tool: Tool | undefined,
17-
toolCallState: ToolCallState,
18-
) {
19-
if (!tool) return "Agent tool use";
20-
21-
const defaultToolDescription = (
22-
<>
23-
<code>{tool.displayTitle ?? tool.function.name}</code> <span>tool</span>
24-
</>
25-
);
26-
27-
const futureMessage = tool.wouldLikeTo ? (
28-
Mustache.render(tool.wouldLikeTo, toolCallState.parsedArgs)
29-
) : (
30-
<>
31-
<span>use the</span> {defaultToolDescription}
32-
</>
33-
);
34-
35-
let intro = "";
36-
let message: ReactNode = "";
37-
38-
if (
39-
toolCallState.status === "done" ||
40-
(tool.isInstant && toolCallState.status === "calling")
41-
) {
42-
intro = "";
43-
message = tool.hasAlready ? (
44-
Mustache.render(tool.hasAlready, toolCallState.parsedArgs)
45-
) : (
46-
<>
47-
<span>used the</span> {defaultToolDescription}
48-
</>
49-
);
50-
} else if (toolCallState.status === "generating") {
51-
intro = "is generating output to";
52-
message = futureMessage;
53-
} else if (toolCallState.status === "generated") {
54-
intro = "wants to";
55-
message = futureMessage;
56-
} else if (toolCallState.status === "calling") {
57-
intro = "is";
58-
message = tool.isCurrently ? (
59-
Mustache.render(tool.isCurrently, toolCallState.parsedArgs)
60-
) : (
61-
<>
62-
<span>calling the</span> {defaultToolDescription}
63-
</>
64-
);
65-
} else if (
66-
toolCallState.status === "canceled" ||
67-
toolCallState.status === "errored"
68-
) {
69-
intro = "tried to";
70-
message = futureMessage;
71-
}
72-
return (
73-
<div className="block">
74-
<span>Continue</span> {intro} {message}
75-
</div>
76-
);
77-
}
78-
79-
export function ToolCallDisplay(props: ToolCallDisplayProps) {
80-
const [isExpanded, setIsExpanded] = useState(false);
81-
const availableTools = useAppSelector((state) => state.config.config.tools);
82-
83-
const tool = useMemo(() => {
84-
return availableTools.find(
85-
(tool) => props.toolCall.function?.name === tool.function.name,
86-
);
87-
}, [availableTools, props.toolCall]);
88-
89-
const statusMessage = useMemo(() => {
90-
return getToolCallStatusMessage(tool, props.toolCallState);
91-
}, [props.toolCallState, tool]);
13+
export function ToolCallDisplay({
14+
tool,
15+
toolCallState,
16+
children,
17+
icon,
18+
}: ToolCallDisplayProps) {
19+
const [argsExpanded, setArgsExpanded] = useState(false);
9220

9321
const args: [string, any][] = useMemo(() => {
94-
return Object.entries(props.toolCallState.parsedArgs);
95-
}, [props.toolCallState.parsedArgs]);
96-
97-
const argsTooltipId = useMemo(() => {
98-
return "args-hover-" + props.toolCallState.toolCallId;
99-
}, [props.toolCallState]);
22+
return Object.entries(toolCallState.parsedArgs);
23+
}, [toolCallState.parsedArgs]);
10024

10125
return (
10226
<>
10327
<div className="relative flex flex-col justify-center p-4 pb-0">
10428
<div className="mb-4 flex flex-col">
10529
<div className="flex flex-row items-center justify-between gap-3">
10630
<div className="flex flex-row gap-2">
107-
<div
108-
style={{
109-
width: `16px`,
110-
height: `16px`,
111-
fontWeight: "bolder",
112-
marginTop: "1px",
113-
flexShrink: 0,
114-
}}
115-
>
116-
{props.icon}
31+
<div className="mt-[1px] h-4 w-4 flex-shrink-0 font-semibold">
32+
{icon}
11733
</div>
11834
{tool?.faviconUrl && (
11935
<img src={tool.faviconUrl} className="h-4 w-4 rounded-sm" />
12036
)}
12137
<div className="flex" data-testid="tool-call-status-message">
122-
{statusMessage}
38+
<ToolCallStatusMessage
39+
tool={tool}
40+
toolCallState={toolCallState}
41+
/>
12342
</div>
12443
</div>
12544
{!!args.length ? (
126-
<div
127-
data-tooltip-id={argsTooltipId}
128-
onClick={() => setIsExpanded(!isExpanded)}
129-
className="ml-2 cursor-pointer hover:opacity-80"
130-
>
131-
{isExpanded ? (
132-
<ChevronUpIcon className="h-4 w-4" />
133-
) : (
134-
<ChevronDownIcon className="h-4 w-4" />
135-
)}
136-
</div>
45+
<ArgsToggleIcon
46+
isShowing={argsExpanded}
47+
setIsShowing={setArgsExpanded}
48+
toolCallId={toolCallState.toolCallId}
49+
/>
13750
) : null}
138-
<ToolTip id={argsTooltipId}>
139-
{isExpanded ? "Hide args" : "Show args"}
140-
</ToolTip>
14151
</div>
142-
143-
{isExpanded && !!args.length && (
144-
<div className="ml-7 mt-1">
145-
{args.map(([key, value]) => (
146-
<div key={key} className="flex gap-2 py-0.5">
147-
<span className="text-lightgray">{key}:</span>
148-
<code className="line-clamp-1">{value.toString()}</code>
149-
</div>
150-
))}
151-
</div>
52+
{argsExpanded && !!args.length && (
53+
<ArgsItems args={args} isShowing={argsExpanded} />
15254
)}
15355
</div>
154-
<div>{props.children}</div>
56+
<div>{children}</div>
15557
</div>
15658
</>
15759
);

0 commit comments

Comments
 (0)