Expanded ToolCall options (#2457)

This commit is contained in:
Oliver
2025-05-07 11:27:17 -05:00
committed by GitHub
parent bf42e8e76c
commit 3419385af5
6 changed files with 177 additions and 32 deletions
+10 -8
View File
@@ -105,13 +105,13 @@ export default function GooseMessage({
<div className="flex flex-col w-full"> <div className="flex flex-col w-full">
{textContent && ( {textContent && (
<div className="flex flex-col group"> <div className="flex flex-col group">
<div className={`goose-message-content py-2`}> <div className={`goose-message-content pt-2`}>
<div ref={contentRef}>{<MarkdownContent content={textContent} />}</div> <div ref={contentRef}>{<MarkdownContent content={textContent} />}</div>
</div> </div>
{/* Only show MessageCopyLink if there's text content and no tool requests/responses */} {/* Only show MessageCopyLink if there's text content and no tool requests/responses */}
<div className="relative flex justify-end z-[-1]"> <div className="relative flex justify-start z-[-1]">
{toolRequests.length === 0 && ( {toolRequests.length === 0 && (
<div className="absolute left-0 text-xs text-textSubtle pt-1 transition-all duration-200 group-hover:-translate-y-4 group-hover:opacity-0"> <div className="text-xs text-textSubtle pt-1 transition-all duration-200 group-hover:-translate-y-4 group-hover:opacity-0">
{timestamp} {timestamp}
</div> </div>
)} )}
@@ -126,20 +126,22 @@ export default function GooseMessage({
{toolRequests.length > 0 && ( {toolRequests.length > 0 && (
<div className="relative flex flex-col w-full"> <div className="relative flex flex-col w-full">
<div className={`goose-message-tool bg-bgSubtle rounded px-2 py-2 mt-2`}> {toolRequests.map((toolRequest) => (
{toolRequests.map((toolRequest) => ( <div
className={`goose-message-tool bg-bgSubtle rounded px-2 py-2 mb-2`}
key={toolRequest.id}
>
<ToolCallWithResponse <ToolCallWithResponse
// If the message is resumed and not matched tool response, it means the tool is broken or cancelled. // If the message is resumed and not matched tool response, it means the tool is broken or cancelled.
isCancelledMessage={ isCancelledMessage={
messageIndex < messageHistoryIndex && messageIndex < messageHistoryIndex &&
toolResponsesMap.get(toolRequest.id) == undefined toolResponsesMap.get(toolRequest.id) == undefined
} }
key={toolRequest.id}
toolRequest={toolRequest} toolRequest={toolRequest}
toolResponse={toolResponsesMap.get(toolRequest.id)} toolResponse={toolResponsesMap.get(toolRequest.id)}
/> />
))} </div>
</div> ))}
<div className="text-xs text-textSubtle pt-1 transition-all duration-200 group-hover:-translate-y-4 group-hover:opacity-0"> <div className="text-xs text-textSubtle pt-1 transition-all duration-200 group-hover:-translate-y-4 group-hover:opacity-0">
{timestamp} {timestamp}
</div> </div>
@@ -34,24 +34,24 @@ export default function ToolCallWithResponse({
interface ToolCallExpandableProps { interface ToolCallExpandableProps {
label: string | React.ReactNode; label: string | React.ReactNode;
defaultExpanded?: boolean; isStartExpanded?: boolean;
forceExpand?: boolean; isForceExpand?: boolean;
children: React.ReactNode; children: React.ReactNode;
className?: string; className?: string;
} }
function ToolCallExpandable({ function ToolCallExpandable({
label, label,
defaultExpanded = false, isStartExpanded = false,
forceExpand, isForceExpand,
children, children,
className = '', className = '',
}: ToolCallExpandableProps) { }: ToolCallExpandableProps) {
const [isExpanded, setIsExpanded] = React.useState(defaultExpanded); const [isExpanded, setIsExpanded] = React.useState(isStartExpanded);
const toggleExpand = () => setIsExpanded((prev) => !prev); const toggleExpand = () => setIsExpanded((prev) => !prev);
React.useEffect(() => { React.useEffect(() => {
if (forceExpand) setIsExpanded(true); if (isForceExpand) setIsExpanded(true);
}, [forceExpand]); }, [isForceExpand]);
return ( return (
<div className={className}> <div className={className}>
@@ -74,12 +74,23 @@ interface ToolCallViewProps {
} }
function ToolCallView({ isCancelledMessage, toolCall, toolResponse }: ToolCallViewProps) { function ToolCallView({ isCancelledMessage, toolCall, toolResponse }: ToolCallViewProps) {
const responseStyle = localStorage.getItem('response_style');
const isExpandToolDetails = (() => {
switch (responseStyle) {
case 'concise':
return false;
case 'detailed':
default:
return true;
}
})();
const isToolDetails = Object.entries(toolCall?.arguments).length > 0; const isToolDetails = Object.entries(toolCall?.arguments).length > 0;
const loadingStatus: LoadingStatus = !toolResponse?.toolResult.status const loadingStatus: LoadingStatus = !toolResponse?.toolResult.status
? 'loading' ? 'loading'
: toolResponse?.toolResult.status; : toolResponse?.toolResult.status;
const toolResults: { result: Content; defaultExpanded: boolean }[] = const toolResults: { result: Content; isExpandToolResults: boolean }[] =
loadingStatus === 'success' && Array.isArray(toolResponse?.toolResult.value) loadingStatus === 'success' && Array.isArray(toolResponse?.toolResult.value)
? toolResponse.toolResult.value ? toolResponse.toolResult.value
.filter((item) => { .filter((item) => {
@@ -88,16 +99,16 @@ function ToolCallView({ isCancelledMessage, toolCall, toolResponse }: ToolCallVi
}) })
.map((item) => ({ .map((item) => ({
result: item, result: item,
defaultExpanded: ((item.annotations?.priority as number | undefined) ?? -1) >= 0.5, isExpandToolResults: ((item.annotations?.priority as number | undefined) ?? -1) >= 0.5,
})) }))
: []; : [];
const shouldExpand = toolResults.some((v) => v.defaultExpanded); const isShouldExpand = isExpandToolDetails || toolResults.some((v) => v.isExpandToolResults);
return ( return (
<ToolCallExpandable <ToolCallExpandable
defaultExpanded={shouldExpand} isStartExpanded={isShouldExpand}
forceExpand={shouldExpand} isForceExpand={isShouldExpand}
label={ label={
<> <>
<Dot size={2} loadingStatus={loadingStatus} /> <Dot size={2} loadingStatus={loadingStatus} />
@@ -110,21 +121,24 @@ function ToolCallView({ isCancelledMessage, toolCall, toolResponse }: ToolCallVi
{/* Tool Details */} {/* Tool Details */}
{isToolDetails && ( {isToolDetails && (
<div className="bg-bgStandard rounded-t mt-1"> <div className="bg-bgStandard rounded-t mt-1">
<ToolDetailsView toolCall={toolCall} /> <ToolDetailsView toolCall={toolCall} isStartExpanded={isExpandToolDetails} />
</div> </div>
)} )}
{/* Tool Output */} {/* Tool Output */}
{!isCancelledMessage && ( {!isCancelledMessage && (
<> <>
{toolResults.map(({ result, defaultExpanded }, index) => { {toolResults.map(({ result, isExpandToolResults }, index) => {
const isLast = index === toolResults.length - 1; const isLast = index === toolResults.length - 1;
return ( return (
<div <div
key={index} key={index}
className={`bg-bgStandard mt-1 ${isToolDetails ? 'rounded-t-none' : ''} ${isLast ? 'rounded-b' : ''}`} className={`bg-bgStandard mt-1
${isToolDetails || index > 0 ? '' : 'rounded-t'}
${isLast ? 'rounded-b' : ''}
`}
> >
<ToolResultView result={result} defaultExpanded={defaultExpanded} /> <ToolResultView result={result} isStartExpanded={isExpandToolResults} />
</div> </div>
); );
})} })}
@@ -139,11 +153,16 @@ interface ToolDetailsViewProps {
name: string; name: string;
arguments: Record<string, unknown>; arguments: Record<string, unknown>;
}; };
isStartExpanded: boolean;
} }
function ToolDetailsView({ toolCall }: ToolDetailsViewProps) { function ToolDetailsView({ toolCall, isStartExpanded }: ToolDetailsViewProps) {
return ( return (
<ToolCallExpandable label="Tool Details" className="pl-[19px] py-1"> <ToolCallExpandable
label="Tool Details"
className="pl-[19px] py-1"
isStartExpanded={isStartExpanded}
>
{toolCall.arguments && <ToolCallArguments args={toolCall.arguments} />} {toolCall.arguments && <ToolCallArguments args={toolCall.arguments} />}
</ToolCallExpandable> </ToolCallExpandable>
); );
@@ -151,14 +170,14 @@ function ToolDetailsView({ toolCall }: ToolDetailsViewProps) {
interface ToolResultViewProps { interface ToolResultViewProps {
result: Content; result: Content;
defaultExpanded: boolean; isStartExpanded: boolean;
} }
function ToolResultView({ result, defaultExpanded }: ToolResultViewProps) { function ToolResultView({ result, isStartExpanded }: ToolResultViewProps) {
return ( return (
<ToolCallExpandable <ToolCallExpandable
label={<span className="pl-[19px] py-1">Output</span>} label={<span className="pl-[19px] py-1">Output</span>}
defaultExpanded={defaultExpanded} isStartExpanded={isStartExpanded}
> >
<div className="bg-bgApp rounded-b pl-[19px] pr-2 py-4"> <div className="bg-bgApp rounded-b pl-[19px] pr-2 py-4">
{result.type === 'text' && result.text && ( {result.type === 'text' && result.text && (
@@ -5,6 +5,7 @@ import ExtensionsSection from './extensions/ExtensionsSection';
import ModelsSection from './models/ModelsSection'; import ModelsSection from './models/ModelsSection';
import { ModeSection } from './mode/ModeSection'; import { ModeSection } from './mode/ModeSection';
import SessionSharingSection from './sessions/SessionSharingSection'; import SessionSharingSection from './sessions/SessionSharingSection';
import { ResponseStylesSection } from './response_styles/ResponseStylesSection';
import { ExtensionConfig } from '../../api'; import { ExtensionConfig } from '../../api';
import MoreMenuLayout from '../more_menu/MoreMenuLayout'; import MoreMenuLayout from '../more_menu/MoreMenuLayout';
@@ -47,6 +48,8 @@ export default function SettingsView({
<ModeSection setView={setView} /> <ModeSection setView={setView} />
{/*Session sharing*/} {/*Session sharing*/}
<SessionSharingSection /> <SessionSharingSection />
{/* Response Styles */}
<ResponseStylesSection />
</div> </div>
</div> </div>
</div> </div>
@@ -0,0 +1,75 @@
import { useEffect, useState } from 'react';
export interface ResponseStyle {
key: string;
label: string;
description: string;
}
export const all_response_styles: ResponseStyle[] = [
{
key: 'detailed',
label: 'Detailed',
description: 'Tool calls are by default shown open to expose details',
},
{
key: 'concise',
label: 'Concise',
description: 'Tool calls are by default closed and only show the tool used',
},
];
interface ResponseStyleSelectionItemProps {
currentStyle: string;
style: ResponseStyle;
showDescription: boolean;
handleStyleChange: (newStyle: string) => void;
}
export function ResponseStyleSelectionItem({
currentStyle,
style,
showDescription,
handleStyleChange,
}: ResponseStyleSelectionItemProps) {
const [checked, setChecked] = useState(currentStyle === style.key);
useEffect(() => {
setChecked(currentStyle === style.key);
}, [currentStyle, style.key]);
return (
<div className="group hover:cursor-pointer">
<div
className="flex items-center justify-between text-textStandard py-2 px-4 hover:bg-bgSubtle"
onClick={() => handleStyleChange(style.key)}
>
<div className="flex">
<div>
<h3 className="text-textStandard">{style.label}</h3>
{showDescription && (
<p className="text-xs text-textSubtle max-w-md mt-[2px]">{style.description}</p>
)}
</div>
</div>
<div className="relative flex items-center gap-2">
<input
type="radio"
name="responseStyles"
value={style.key}
checked={checked}
onChange={() => handleStyleChange(style.key)}
className="peer sr-only"
/>
<div
className="h-4 w-4 rounded-full border border-borderStandard
peer-checked:border-[6px] peer-checked:border-black dark:peer-checked:border-white
peer-checked:bg-white dark:peer-checked:bg-black
transition-all duration-200 ease-in-out group-hover:border-borderProminent"
></div>
</div>
</div>
</div>
);
}
@@ -0,0 +1,46 @@
import { useEffect, useState } from 'react';
import { all_response_styles, ResponseStyleSelectionItem } from './ResponseStyleSelectionItem';
export const ResponseStylesSection = () => {
const [currentStyle, setCurrentStyle] = useState('detailed');
useEffect(() => {
const savedStyle = localStorage.getItem('response_style');
if (savedStyle) {
try {
setCurrentStyle(savedStyle);
} catch (error) {
console.error('Error parsing response style:', error);
}
}
}, []);
const handleStyleChange = async (newStyle: string) => {
setCurrentStyle(newStyle);
localStorage.setItem('response_style', newStyle);
};
return (
<section id="responseStyles" className="px-8">
<div className="flex justify-between items-center mb-2">
<h2 className="text-xl font-medium text-textStandard">Response Styles</h2>
</div>
<div className="pb-8">
<p className="text-sm text-textStandard mb-6">
Choose how Goose should format and style its responses
</p>
<div>
{all_response_styles.map((style) => (
<ResponseStyleSelectionItem
key={style.key}
style={style}
currentStyle={currentStyle}
showDescription={true}
handleStyleChange={handleStyleChange}
/>
))}
</div>
</div>
</section>
);
};
@@ -80,13 +80,13 @@ export default function SessionSharingSection() {
}; };
return ( return (
<section id="session-sharing"> <section id="session-sharing" className="px-8">
{/*Title*/} {/*Title*/}
<div className="flex justify-between items-center mb-2 px-8"> <div className="flex justify-between items-center mb-2">
<h2 className="text-xl font-medium text-textStandard">Session sharing</h2> <h2 className="text-xl font-medium text-textStandard">Session sharing</h2>
</div> </div>
<div className="px-8"> <div className="border-b border-borderSubtle pb-8">
{envBaseUrlShare ? ( {envBaseUrlShare ? (
<p className="text-sm text-textStandard mb-4"> <p className="text-sm text-textStandard mb-4">
Session sharing is configured but fully opt-in your sessions are only shared when you Session sharing is configured but fully opt-in your sessions are only shared when you