fix(agent-v2): use icon_url for image avatars in the configure preview (#42977)

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: yyh <yuanyouhuilyz@gmail.com>
This commit is contained in:
authored and GitHub committed 2026-09-26 12:36:08 +00:00
1 parent 3960b57102
commit 7a7dc6011a
9 files changed
+211 -60

No files matched your search

@@ -742,13 +742,6 @@ function WorkflowInlineAgentConfigureWorkspaceContent({
<AgentConfigureRightPanelChat
agentId={agentId}
answerActionPosition="below"
agentIcon={composerState?.agent?.icon}
agentIconBackground={composerState?.agent?.icon_background}
agentIconType={
composerState?.agent?.icon_type as Parameters<
typeof AgentConfigureRightPanelChat
>[0]['agentIconType']
}
agentName={composerState?.agent?.name}
agentSoulConfig={buildDraft.agentSoulConfig}
clearChatList={clearPreviewChat}
@@ -137,6 +137,47 @@ describe('AgentDetailSection', () => {
expect(agentAvatar).toHaveClass('h-10', 'w-10', 'rounded-full')
})
it('renders an uploaded sidebar avatar using its signed URL', () => {
mocks.queryData = createAgent({
icon_type: 'image',
icon: 'uploaded-file-id',
icon_url: 'https://files.example.com/avatar.png?sign=signature',
})
renderAgentDetailSection()
expect(screen.getByRole('img', { hidden: true })).toHaveAttribute(
'src',
'https://files.example.com/avatar.png?sign=signature',
)
})
it('preserves an external sidebar avatar when icon_url is null', () => {
mocks.queryData = createAgent({
icon_type: 'link',
icon: 'https://example.com/avatar.png',
icon_url: null,
})
renderAgentDetailSection()
expect(screen.getByRole('img', { hidden: true })).toHaveAttribute(
'src',
'https://example.com/avatar.png',
)
})
it('shows a fallback without exposing the file ID when the signed URL is missing', () => {
mocks.queryData = createAgent({
icon_type: 'image',
icon: 'uploaded-file-id',
icon_url: null,
})
renderAgentDetailSection()
expect(screen.queryByRole('img', { hidden: true })).not.toBeInTheDocument()
expect(screen.queryByText('uploaded-file-id')).not.toBeInTheDocument()
expect(screen.getByText('🤖')).toBeInTheDocument()
})
it.each([null, '', ' '])(
'omits an empty role while keeping the agent accessible (%s)',
(role) => {
@@ -2,7 +2,6 @@
import type {
AgentAppDetailWithSite,
AgentIconType,
AgentSoulConfig,
} from '@dify/contracts/api/console/agent/types.gen'
import type { useAgentConfigureData } from '../hooks'
@@ -152,7 +151,6 @@ function AgentConfigurePageComposerSession({
normalAgentSoulConfig: agentSoulConfig,
onModeChange: onRightPanelModeChange,
})
const agentIconType = agentQuery.data?.icon_type as AgentIconType | null | undefined
const refreshDebugConversationMutation = useMutation(
consoleQuery.agent.byAgentId.debugConversation.refresh.post.mutationOptions({
onSuccess: ({
@@ -231,7 +229,6 @@ function AgentConfigurePageComposerSession({
>
<AgentConfigurePageComposerContent
agentId={agentId}
agentIconType={agentIconType}
buildDraft={buildDraft}
configureData={configureData}
isRefreshingDebugConversation={
@@ -254,7 +251,6 @@ function AgentConfigurePageComposerSession({
function AgentConfigurePageComposerContent({
agentId,
agentIconType,
buildDraft,
configureData,
isRefreshingDebugConversation,
@@ -269,7 +265,6 @@ function AgentConfigurePageComposerContent({
onSelectVersion,
}: {
agentId: string
agentIconType: AgentIconType | null | undefined
buildDraft: ReturnType<typeof useAgentConfigureBuildDraftData>
configureData: ReturnType<typeof useAgentConfigureData>
isRefreshingDebugConversation: boolean
@@ -554,9 +549,6 @@ function AgentConfigurePageComposerContent({
<AgentConfigureRightPanelChat
agentId={agentId}
answerActionPosition="below"
agentIcon={agentQuery.data?.icon}
agentIconBackground={agentQuery.data?.icon_background}
agentIconType={agentIconType}
agentName={agentQuery.data?.name}
agentSoulConfig={buildDraft.agentSoulConfig}
clearChatList={clearChatByMode[rightPanelChatMode]}
@@ -1,7 +1,11 @@
import type { ReactElement } from 'react'
import type { AgentChatMessageSender } from '../chat-conversation'
import type { AgentChatRuntimeProps } from '../chat-runtime'
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
import { screen } from '@testing-library/react'
import { render } from '@/test/console/render'
import { consoleQuery } from '@/service/console'
import { render as renderConsole } from '@/test/console/render'
import { createAgentFixture } from '@/test/fixtures/agent'
import { AgentBuildChat } from '../build-chat'
import { sendBuildChatMessage } from '../build-chat-request'
import { AgentPreviewChat } from '../preview-chat'
@@ -28,6 +32,23 @@ const commonProps = {
onClearChatListChange: vi.fn(),
}
function render(ui: ReactElement) {
const queryClient = new QueryClient({
defaultOptions: { queries: { staleTime: Infinity, retry: false } },
})
queryClient.setQueryData(
consoleQuery.agent.byAgentId.get.queryKey({
input: { params: { agent_id: commonProps.agentId } },
}),
createAgentFixture({ name: 'Research Agent' }),
)
return renderConsole(ui, {
wrapper: ({ children }) => (
<QueryClientProvider client={queryClient}>{children}</QueryClientProvider>
),
})
}
describe('Agent chat mode request routing', () => {
beforeEach(() => {
runtimePropsMock.mockClear()
@@ -56,12 +77,11 @@ describe('Agent chat mode request routing', () => {
})
it('should show the unconfigured notice below the Preview description', () => {
render(<AgentPreviewChat {...commonProps} agentName="Research Agent" />)
render(<AgentPreviewChat {...commonProps} />)
const renderEmptyState = runtimePropsMock.mock.calls.at(-1)?.[0].renderEmptyState
const emptyStateView = render(
renderEmptyState({
agentName: 'Research Agent',
showUnconfiguredNotice: true,
}),
)
@@ -79,7 +99,6 @@ describe('Agent chat mode request routing', () => {
emptyStateView.rerender(
renderEmptyState({
agentName: 'Research Agent',
showUnconfiguredNotice: false,
}),
)
@@ -0,0 +1,115 @@
import type { AgentChatRuntimeProps } from '../chat-runtime'
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
import { act, screen } from '@testing-library/react'
import { consoleQuery } from '@/service/console'
import { render } from '@/test/console/render'
import { createAgentFixture } from '@/test/fixtures/agent'
import { AgentPreviewChat } from '../preview-chat'
vi.mock('../chat-runtime', () => ({
AgentChatRuntime: ({ renderEmptyState }: AgentChatRuntimeProps) =>
renderEmptyState({ showUnconfiguredNotice: false }),
}))
const commonProps = {
agentId: 'agent-1',
clearChatList: false,
onClearChatListChange: vi.fn(),
}
const agentQueryOptions = consoleQuery.agent.byAgentId.get.queryOptions({
input: { params: { agent_id: commonProps.agentId } },
})
function renderPreview(agent: ReturnType<typeof createAgentFixture>) {
const queryClient = new QueryClient({
defaultOptions: { queries: { staleTime: Infinity, retry: false } },
})
queryClient.setQueryData(agentQueryOptions.queryKey, agent)
return {
queryClient,
...render(
<QueryClientProvider client={queryClient}>
<AgentPreviewChat {...commonProps} />
</QueryClientProvider>,
),
}
}
describe('Agent preview avatar', () => {
it('renders an uploaded image from its signed URL instead of its file ID', () => {
renderPreview(
createAgentFixture({
icon_type: 'image',
icon: 'uploaded-file-id',
icon_url: 'https://files.example.com/avatar.png?sign=signature',
}),
)
expect(screen.getByRole('img', { name: 'app icon' })).toHaveAttribute(
'src',
'https://files.example.com/avatar.png?sign=signature',
)
})
it('renders an external image from the raw icon when icon_url is null', () => {
renderPreview(
createAgentFixture({
icon_type: 'link',
icon: 'https://example.com/avatar.png',
icon_url: null,
}),
)
expect(screen.getByRole('img', { name: 'app icon' })).toHaveAttribute(
'src',
'https://example.com/avatar.png',
)
})
it('shows a fallback without exposing the file ID when its signed URL is unavailable', () => {
renderPreview(
createAgentFixture({
icon_type: 'image',
icon: 'uploaded-file-id',
icon_url: null,
}),
)
expect(screen.queryByRole('img', { name: 'app icon' })).not.toBeInTheDocument()
expect(screen.queryByText('uploaded-file-id')).not.toBeInTheDocument()
expect(screen.getByText('🤖')).toBeInTheDocument()
})
it('updates the avatar and name when the shared Agent query changes', async () => {
const { queryClient } = renderPreview(
createAgentFixture({
name: 'Original Agent',
icon_type: 'image',
icon: 'uploaded-file-id',
icon_url: 'https://files.example.com/avatar.png?sign=signature',
}),
)
expect(screen.getByRole('img', { name: 'app icon' })).toBeInTheDocument()
expect(screen.getByText('Original Agent')).toBeInTheDocument()
await act(async () => {
queryClient.setQueryData(
agentQueryOptions.queryKey,
createAgentFixture({
name: 'Updated Agent',
icon_type: 'emoji',
icon: '🚀',
icon_url: null,
}),
)
})
expect(await screen.findByText('Updated Agent')).toBeInTheDocument()
expect(screen.queryByText('Original Agent')).not.toBeInTheDocument()
expect(screen.queryByRole('img', { name: 'app icon' })).not.toBeInTheDocument()
expect(screen.getByText('🚀')).toBeInTheDocument()
})
})
@@ -1,6 +1,6 @@
'use client'
import type { AgentIconType, AgentSoulConfig } from '@dify/contracts/api/console/agent/types.gen'
import type { AgentSoulConfig } from '@dify/contracts/api/console/agent/types.gen'
import type { ReactNode, Ref } from 'react'
import type { AgentChatMessageSender, AgentPreviewChatController } from './chat-conversation'
import type { AnswerActionPosition } from '@/app/components/base/chat/chat/answer/operation'
@@ -12,19 +12,12 @@ import { getFormattedAgentDebugChatTree } from './chat-history'
import { AgentPreviewChatSession } from './chat-session'
export type AgentChatRuntimeEmptyStateProps = {
agentIcon?: string | null
agentIconBackground?: string | null
agentIconType?: AgentIconType | null
agentName?: string
showUnconfiguredNotice: boolean
}
export type AgentChatRuntimeProps = {
agentId: string
answerActionPosition?: AnswerActionPosition
agentIcon?: string | null
agentIconBackground?: string | null
agentIconType?: AgentIconType | null
agentName?: string
agentSoulConfig?: AgentSoulConfig
clearChatList: boolean
@@ -49,9 +42,6 @@ export type AgentChatRuntimeProps = {
export function AgentChatRuntime({
agentId,
answerActionPosition,
agentIcon,
agentIconBackground,
agentIconType,
agentName,
agentSoulConfig,
clearChatList,
@@ -118,9 +108,6 @@ export function AgentChatRuntime({
conversationSessionKey={conversationSessionKey}
agentId={agentId}
answerActionPosition={answerActionPosition}
agentIcon={agentIcon}
agentIconBackground={agentIconBackground}
agentIconType={agentIconType}
agentName={agentName}
agentSoulConfig={agentSoulConfig}
clearChatList={clearChatList}
@@ -1,6 +1,6 @@
'use client'
import type { AgentIconType, AgentSoulConfig } from '@dify/contracts/api/console/agent/types.gen'
import type { AgentSoulConfig } from '@dify/contracts/api/console/agent/types.gen'
import type { ReactNode, Ref } from 'react'
import type {
AgentChatMessageSender,
@@ -29,9 +29,6 @@ export function AgentPreviewChatSession({
conversationSessionKey,
agentId,
answerActionPosition,
agentIcon,
agentIconBackground,
agentIconType,
agentName,
agentSoulConfig,
clearChatList,
@@ -57,10 +54,7 @@ export function AgentPreviewChatSession({
conversationSessionKey: string
agentId: string
answerActionPosition?: AgentChatRuntimeProps['answerActionPosition']
agentIcon?: string | null
agentIconBackground?: string | null
agentIconType?: AgentIconType | null
agentName?: string
agentName?: AgentChatRuntimeProps['agentName']
agentSoulConfig?: AgentSoulConfig
clearChatList: boolean
controllerRef?: Ref<AgentPreviewChatController>
@@ -234,10 +228,6 @@ export function AgentPreviewChatSession({
{isEmptyChat && (
<div className="w-full p-3 pb-0">
{renderEmptyState({
agentIcon,
agentIconBackground,
agentIconType,
agentName,
showUnconfiguredNotice,
})}
</div>
@@ -1,8 +1,10 @@
'use client'
import type { AgentChatRuntimeEmptyStateProps, AgentChatRuntimeProps } from './chat-runtime'
import { useQuery } from '@tanstack/react-query'
import { useTranslation } from 'react-i18next'
import AppIcon from '@/app/components/base/app-icon'
import { consoleQuery } from '@/service/console'
import { AgentChatRuntime } from './chat-runtime'
import { sendPreviewChatMessage } from './preview-chat-request'
import { AgentUnconfiguredNotice } from './unconfigured-notice'
@@ -13,15 +15,22 @@ type AgentPreviewChatProps = Omit<
>
function AgentPreviewChatEmptyState({
agentIcon,
agentIconBackground,
agentIconType,
agentName,
agentId,
showUnconfiguredNotice,
}: AgentChatRuntimeEmptyStateProps) {
}: AgentChatRuntimeEmptyStateProps & { agentId: AgentChatRuntimeProps['agentId'] }) {
const { t } = useTranslation(['agentV2'])
const imageUrl = agentIconType === 'image' || agentIconType === 'link' ? agentIcon : undefined
const iconType = imageUrl ? 'image' : agentIconType
const { data: agent } = useQuery(
consoleQuery.agent.byAgentId.get.queryOptions({
input: { params: { agent_id: agentId } },
}),
)
const imageUrl =
agent?.icon_type === 'image'
? agent.icon_url
: agent?.icon_type === 'link'
? agent.icon
: undefined
const iconType = imageUrl ? 'image' : agent?.icon_type
return (
<>
@@ -29,13 +38,13 @@ function AgentPreviewChatEmptyState({
size="xxl"
rounded
iconType={iconType}
icon={agentIcon ?? undefined}
background={agentIconBackground}
icon={agent?.icon_type === 'emoji' ? (agent.icon ?? undefined) : undefined}
background={agent?.icon_background}
imageUrl={imageUrl}
className="bg-background-default"
/>
<div className="mt-3 max-w-full truncate system-md-medium text-text-secondary">
{agentName || t(($) => $['agentDetail.configure.preview.empty.defaultAgentName'])}
{agent?.name || t(($) => $['agentDetail.configure.preview.empty.defaultAgentName'])}
</div>
<p className="mt-1 max-w-full body-md-regular text-text-tertiary">
{t(($) => $['agentDetail.configure.preview.empty.description'])}
@@ -57,7 +66,9 @@ export function AgentPreviewChat(props: AgentPreviewChatProps) {
name: agentName,
})}
sendMessage={sendPreviewChatMessage}
renderEmptyState={(emptyStateProps) => <AgentPreviewChatEmptyState {...emptyStateProps} />}
renderEmptyState={(emptyStateProps) => (
<AgentPreviewChatEmptyState {...emptyStateProps} agentId={props.agentId} />
)}
/>
)
}
@@ -1,6 +1,5 @@
'use client'
import type { AgentIconType } from '@dify/contracts/api/console/agent/types.gen'
import type { AgentDetailSectionKey } from './section'
import {
Breadcrumb,
@@ -179,8 +178,12 @@ export function AgentDetailSection({ expand = true }: AgentDetailSectionProps) {
sectionAccess[item.labelKey.slice('agentDetail.sections.'.length) as AgentDetailSectionKey],
)
const imageUrl =
agent?.icon_type === 'image' || agent?.icon_type === 'link' ? agent.icon : undefined
const iconType = (imageUrl ? 'image' : agent?.icon_type) as AgentIconType | null | undefined
agent?.icon_type === 'image'
? agent.icon_url
: agent?.icon_type === 'link'
? agent.icon
: undefined
const iconType = imageUrl ? 'image' : agent?.icon_type
return (
<div className={cn('flex min-h-0 flex-1 flex-col', expand ? 'px-2 pb-2' : 'pb-2')}>
@@ -207,7 +210,7 @@ export function AgentDetailSection({ expand = true }: AgentDetailSectionProps) {
size="large"
rounded
iconType={iconType}
icon={agent?.icon ?? undefined}
icon={agent?.icon_type === 'emoji' ? (agent.icon ?? undefined) : undefined}
background={agent?.icon_background}
imageUrl={imageUrl}
/>