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

View File

@@ -105,13 +105,13 @@ export default function GooseMessage({
<div className="flex flex-col w-full">
{textContent && (
<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>
{/* 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 && (
<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}
</div>
)}
@@ -126,20 +126,22 @@ export default function GooseMessage({
{toolRequests.length > 0 && (
<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
// If the message is resumed and not matched tool response, it means the tool is broken or cancelled.
isCancelledMessage={
messageIndex < messageHistoryIndex &&
toolResponsesMap.get(toolRequest.id) == undefined
}
key={toolRequest.id}
toolRequest={toolRequest}
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">
{timestamp}
</div>

View File

@@ -34,24 +34,24 @@ export default function ToolCallWithResponse({
interface ToolCallExpandableProps {
label: string | React.ReactNode;
defaultExpanded?: boolean;
forceExpand?: boolean;
isStartExpanded?: boolean;
isForceExpand?: boolean;
children: React.ReactNode;
className?: string;
}
function ToolCallExpandable({
label,
defaultExpanded = false,
forceExpand,
isStartExpanded = false,
isForceExpand,
children,
className = '',
}: ToolCallExpandableProps) {
const [isExpanded, setIsExpanded] = React.useState(defaultExpanded);
const [isExpanded, setIsExpanded] = React.useState(isStartExpanded);
const toggleExpand = () => setIsExpanded((prev) => !prev);
React.useEffect(() => {
if (forceExpand) setIsExpanded(true);
}, [forceExpand]);
if (isForceExpand) setIsExpanded(true);
}, [isForceExpand]);
return (
<div className={className}>
@@ -74,12 +74,23 @@ interface 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 loadingStatus: LoadingStatus = !toolResponse?.toolResult.status
? 'loading'
: toolResponse?.toolResult.status;
const toolResults: { result: Content; defaultExpanded: boolean }[] =
const toolResults: { result: Content; isExpandToolResults: boolean }[] =
loadingStatus === 'success' && Array.isArray(toolResponse?.toolResult.value)
? toolResponse.toolResult.value
.filter((item) => {
@@ -88,16 +99,16 @@ function ToolCallView({ isCancelledMessage, toolCall, toolResponse }: ToolCallVi
})
.map((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 (
<ToolCallExpandable
defaultExpanded={shouldExpand}
forceExpand={shouldExpand}
isStartExpanded={isShouldExpand}
isForceExpand={isShouldExpand}
label={
<>
<Dot size={2} loadingStatus={loadingStatus} />
@@ -110,21 +121,24 @@ function ToolCallView({ isCancelledMessage, toolCall, toolResponse }: ToolCallVi
{/* Tool Details */}
{isToolDetails && (
<div className="bg-bgStandard rounded-t mt-1">
<ToolDetailsView toolCall={toolCall} />
<ToolDetailsView toolCall={toolCall} isStartExpanded={isExpandToolDetails} />
</div>
)}
{/* Tool Output */}
{!isCancelledMessage && (
<>
{toolResults.map(({ result, defaultExpanded }, index) => {
{toolResults.map(({ result, isExpandToolResults }, index) => {
const isLast = index === toolResults.length - 1;
return (
<div
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>
);
})}
@@ -139,11 +153,16 @@ interface ToolDetailsViewProps {
name: string;
arguments: Record<string, unknown>;
};
isStartExpanded: boolean;
}
function ToolDetailsView({ toolCall }: ToolDetailsViewProps) {
function ToolDetailsView({ toolCall, isStartExpanded }: ToolDetailsViewProps) {
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} />}
</ToolCallExpandable>
);
@@ -151,14 +170,14 @@ function ToolDetailsView({ toolCall }: ToolDetailsViewProps) {
interface ToolResultViewProps {
result: Content;
defaultExpanded: boolean;
isStartExpanded: boolean;
}
function ToolResultView({ result, defaultExpanded }: ToolResultViewProps) {
function ToolResultView({ result, isStartExpanded }: ToolResultViewProps) {
return (
<ToolCallExpandable
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">
{result.type === 'text' && result.text && (

View File

@@ -5,6 +5,7 @@ import ExtensionsSection from './extensions/ExtensionsSection';
import ModelsSection from './models/ModelsSection';
import { ModeSection } from './mode/ModeSection';
import SessionSharingSection from './sessions/SessionSharingSection';
import { ResponseStylesSection } from './response_styles/ResponseStylesSection';
import { ExtensionConfig } from '../../api';
import MoreMenuLayout from '../more_menu/MoreMenuLayout';
@@ -47,6 +48,8 @@ export default function SettingsView({
<ModeSection setView={setView} />
{/*Session sharing*/}
<SessionSharingSection />
{/* Response Styles */}
<ResponseStylesSection />
</div>
</div>
</div>

View File

@@ -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>
);
}

View File

@@ -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>
);
};

View File

@@ -80,13 +80,13 @@ export default function SessionSharingSection() {
};
return (
<section id="session-sharing">
<section id="session-sharing" className="px-8">
{/*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>
</div>
<div className="px-8">
<div className="border-b border-borderSubtle pb-8">
{envBaseUrlShare ? (
<p className="text-sm text-textStandard mb-4">
Session sharing is configured but fully opt-in your sessions are only shared when you