fix: prevent agent tool info popover from jumping on close (#36389)

Co-authored-by: yyh <yuanyouhuilyz@gmail.com>
This commit is contained in:
Joel
2026-05-19 17:54:52 +08:00
committed by GitHub
parent a328bbbced
commit cae9923e5a
2 changed files with 38 additions and 55 deletions

View File

@@ -12,7 +12,6 @@ import type { ModelConfig } from '@/models/debug'
import type { AgentTool } from '@/types/app'
import { act, render, screen, waitFor } from '@testing-library/react'
import userEvent from '@testing-library/user-event'
import copy from 'copy-to-clipboard'
import * as React from 'react'
import {
useEffect,
@@ -27,6 +26,7 @@ import {
} from '@/config'
import ConfigContext from '@/context/debug-configuration'
import { ModelModeType } from '@/types/app'
import { writeTextToClipboard } from '@/utils/clipboard'
import AgentTools from '../index'
const formattingDispatcherMock = vi.fn()
@@ -99,9 +99,11 @@ vi.mock('../setting-built-in-tool', () => ({
default: (props: SettingBuiltInToolProps) => <SettingBuiltInToolMock {...props} />,
}))
vi.mock('copy-to-clipboard')
vi.mock('@/utils/clipboard', () => ({
writeTextToClipboard: vi.fn().mockResolvedValue(undefined),
}))
const copyMock = copy as Mock
const writeTextToClipboardMock = writeTextToClipboard as Mock
const createToolParameter = (overrides?: Partial<ToolParameter>): ToolParameter => ({
name: 'api_key',
@@ -241,7 +243,7 @@ const renderAgentTools = (initialTools?: AgentTool[]) => {
const hoverInfoIcon = async (rowIndex = 0) => {
const rows = document.querySelectorAll('.group')
const infoTrigger = rows.item(rowIndex)?.querySelector('[data-testid="tool-info-tooltip"]')
const infoTrigger = rows.item(rowIndex)?.querySelector('[aria-label="search"]')
if (!infoTrigger)
throw new Error('Info trigger not found')
await userEvent.hover(infoTrigger as HTMLElement)
@@ -343,13 +345,21 @@ describe('AgentTools', () => {
expect(screen.getByText('Translate Tool')).toBeInTheDocument()
})
it('should copy tool name from tooltip action', async () => {
it('should copy tool name from infotip action', async () => {
renderAgentTools()
await hoverInfoIcon()
const copyButton = await screen.findByText('tools.copyToolName')
await userEvent.click(copyButton)
expect(copyMock).toHaveBeenCalledWith('search')
expect(writeTextToClipboardMock).toHaveBeenCalledWith('search')
})
it('should expose the tool name infotip trigger', () => {
renderAgentTools()
const infoTrigger = screen.getByRole('button', { name: 'search' })
expect(infoTrigger).toBeInTheDocument()
})
it('should toggle tool enabled state via switch', async () => {

View File

@@ -12,9 +12,7 @@ import { Tooltip, TooltipContent, TooltipTrigger } from '@langgenius/dify-ui/too
import {
RiDeleteBinLine,
RiEqualizer2Line,
RiInformation2Line,
} from '@remixicon/react'
import copy from 'copy-to-clipboard'
import { produce } from 'immer'
import * as React from 'react'
import { useCallback, useMemo, useState } from 'react'
@@ -25,6 +23,7 @@ import OperationBtn from '@/app/components/app/configuration/base/operation-btn'
import AppIcon from '@/app/components/base/app-icon'
import { DefaultToolIcon } from '@/app/components/base/icons/src/public/other'
import { AlertTriangle } from '@/app/components/base/icons/src/vender/solid/alertsAndFeedback'
import { Infotip } from '@/app/components/base/infotip'
import Indicator from '@/app/components/header/indicator'
import { CollectionType } from '@/app/components/tools/types'
import { addDefaultValue, toolParametersToFormSchemas } from '@/app/components/tools/utils/to-form-schema'
@@ -34,6 +33,7 @@ import ConfigContext from '@/context/debug-configuration'
import { useMittContextSelector } from '@/context/mitt-context'
import { useAllBuiltInTools, useAllCustomTools, useAllMCPTools, useAllWorkflowTools } from '@/service/use-tools'
import { canFindTool } from '@/utils'
import { writeTextToClipboard } from '@/utils/clipboard'
import { useFormattingChangedDispatcher } from '../../../debug/hooks'
import SettingBuiltInTool from './setting-built-in-tool'
@@ -156,23 +156,9 @@ const AgentTools: FC = () => {
title={(
<div className="flex items-center">
<div className="mr-1">{t('agent.tools.name', { ns: 'appDebug' })}</div>
<Popover>
<PopoverTrigger
openOnHover
aria-label={t('agent.tools.description', { ns: 'appDebug' })}
render={(
<button
type="button"
className="flex h-4 w-4 shrink-0 items-center justify-center rounded-sm p-px outline-hidden hover:bg-state-base-hover focus-visible:ring-1 focus-visible:ring-components-input-border-hover"
>
<span aria-hidden className="i-ri-question-line h-3.5 w-3.5 text-text-quaternary hover:text-text-tertiary" />
</button>
)}
/>
<PopoverContent popupClassName="w-[180px] px-3 py-2 system-xs-regular text-text-tertiary">
{t('agent.tools.description', { ns: 'appDebug' })}
</PopoverContent>
</Popover>
<Infotip aria-label={t('agent.tools.description', { ns: 'appDebug' })}>
{t('agent.tools.description', { ns: 'appDebug' })}
</Infotip>
</div>
)}
headerRight={(
@@ -228,36 +214,23 @@ const AgentTools: FC = () => {
<span className="pr-1.5 system-xs-medium text-text-secondary">{getProviderShowName(item)}</span>
<span className="text-text-tertiary">{item.tool_label}</span>
{!item.isDeleted && !readonly && (
<Popover>
<span className="h-4 w-4">
<PopoverTrigger
openOnHover
aria-label={item.tool_name}
render={(
<button
type="button"
className="ml-0.5 hidden h-4 w-4 items-center justify-center rounded-sm outline-hidden group-hover:inline-flex hover:bg-state-base-hover focus-visible:inline-flex focus-visible:ring-1 focus-visible:ring-components-input-border-hover"
data-testid="tool-info-tooltip"
>
<RiInformation2Line className="h-4 w-4 text-text-tertiary" />
</button>
)}
/>
</span>
<PopoverContent popupClassName="w-[180px] px-3 py-2 system-xs-regular">
<div className="w-[180px]">
<div className="mb-1.5 text-text-secondary">{item.tool_name}</div>
<div className="mb-1.5 text-text-tertiary">{t('toolNameUsageTip', { ns: 'tools' })}</div>
<button
type="button"
className="cursor-pointer rounded-sm border-none bg-transparent p-0 text-left text-text-accent outline-hidden hover:underline focus-visible:ring-1 focus-visible:ring-components-input-border-hover"
onClick={() => copy(item.tool_name)}
>
{t('copyToolName', { ns: 'tools' })}
</button>
</div>
</PopoverContent>
</Popover>
<Infotip
aria-label={item.tool_name}
className="ml-0.5 opacity-0 group-hover:opacity-100 focus-visible:opacity-100"
popupClassName="w-[220px]"
>
<div>
<div className="mb-1.5 text-text-secondary">{item.tool_name}</div>
<div className="mb-1.5 text-text-tertiary">{t('toolNameUsageTip', { ns: 'tools' })}</div>
<button
type="button"
className="cursor-pointer rounded-sm border-none bg-transparent p-0 text-left text-text-accent outline-hidden hover:underline focus-visible:ring-1 focus-visible:ring-components-input-border-hover"
onClick={() => void writeTextToClipboard(item.tool_name)}
>
{t('copyToolName', { ns: 'tools' })}
</button>
</div>
</Infotip>
)}
</div>
</div>