chore(agent-v2): sync nightly updates to main (2026-06-18) (#37610)

Co-authored-by: Joel <iamjoel007@gmail.com>
Co-authored-by: yyh <yuanyouhuilyz@gmail.com>
This commit is contained in:
盐粒 Yanli 2026-06-18 23:34:51 +08:00 committed by GitHub
parent 0df30dd269
commit 8732d1463a
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
80 changed files with 2618 additions and 962 deletions

View File

@ -17,6 +17,7 @@ describe('MenuDialog', () => {
// Assert
expect(screen.getByTestId('dialog-content')).toBeInTheDocument()
expect(screen.getByRole('dialog').children).toHaveLength(1)
})
it('should not render children when show is false', () => {

View File

@ -35,7 +35,6 @@ const MenuDialog = ({
className,
)}
>
<div className="absolute top-0 right-0 h-full w-1/2 bg-components-panel-bg" />
{children}
</DialogContent>
</Dialog>

View File

@ -1,12 +1,15 @@
import type { NodeDefault } from '../../types'
import { act, renderHook } from '@testing-library/react'
import { createNode } from '../../__tests__/fixtures'
import { baseRunningData, renderWorkflowFlowHook, renderWorkflowHook } from '../../__tests__/workflow-test-env'
import { WorkflowRunningStatus } from '../../types'
import { BlockClassificationEnum } from '../../block-selector/types'
import { BlockEnum, WorkflowRunningStatus } from '../../types'
import {
useIsChatMode,
useIsNodeInIteration,
useIsNodeInLoop,
useNodesReadOnly,
useWorkflow,
useWorkflowReadOnly,
} from '../use-workflow'
@ -20,6 +23,20 @@ beforeEach(() => {
mockAppMode = 'workflow'
})
function createNodeDefault(type: BlockEnum): NodeDefault {
return {
metaData: {
classification: BlockClassificationEnum.Default,
sort: 0,
type,
title: type,
author: 'test',
},
defaultValue: {},
checkValid: () => ({ isValid: true }),
}
}
// ---------------------------------------------------------------------------
// useIsChatMode
// ---------------------------------------------------------------------------
@ -169,6 +186,50 @@ describe('useNodesReadOnly', () => {
})
})
// ---------------------------------------------------------------------------
// useWorkflow connection validation
// ---------------------------------------------------------------------------
describe('useWorkflow connection validation', () => {
it('should validate Agent V2 graph nodes against the Agent V2 catalog type', () => {
const agentNode = createNode({
id: 'agent-v2',
data: {
type: BlockEnum.Agent,
title: 'Agent',
desc: '',
agent_node_kind: 'dify_agent',
version: '2',
},
})
const codeNode = createNode({
id: 'code',
data: {
type: BlockEnum.Code,
title: 'Code',
desc: '',
},
})
const { result } = renderWorkflowFlowHook(() => useWorkflow(), {
nodes: [agentNode, codeNode],
edges: [],
hooksStoreProps: {
availableNodesMetaData: {
nodes: [createNodeDefault(BlockEnum.AgentV2), createNodeDefault(BlockEnum.Code)],
},
},
})
expect(result.current.isValidConnection({
source: 'agent-v2',
sourceHandle: 'source',
target: 'code',
targetHandle: 'target',
})).toBe(true)
})
})
// ---------------------------------------------------------------------------
// useIsNodeInIteration
// ---------------------------------------------------------------------------

View File

@ -66,6 +66,7 @@ import {
import { BlockEnum } from '../types'
import {
getDataSourceCheckParams,
getNodeCatalogType,
getToolCheckParams,
getValidTreeNodes,
} from '../utils'
@ -99,10 +100,6 @@ const withFlowType = (moreDataForCheckValid: CheckValidExtraData, flowType?: Flo
}
}
const getNodeMetaType = (data: CommonNodeType) => {
return isAgentV2NodeData(data) ? BlockEnum.AgentV2 : data.type
}
const START_NODE_TYPES: BlockEnum[] = [
BlockEnum.Start,
BlockEnum.TriggerSchedule,
@ -272,7 +269,7 @@ export const useChecklist = (nodes: Node[], edges: Edge[], options?: { flowType?
if (node!.type === CUSTOM_NODE) {
const checkData = getCheckData(node!.data)
const validator = nodesExtraData?.[getNodeMetaType(node!.data) as BlockEnum]?.checkValid
const validator = nodesExtraData?.[getNodeCatalogType(node!.data)]?.checkValid
const isPluginMissing = isNodePluginMissing(node!.data, { builtInTools: buildInTools, customTools, workflowTools, mcpTools, triggerPlugins, dataSourceList })
const errorMessages: string[] = []
@ -556,7 +553,7 @@ export const useChecklistBeforePublish = () => {
}
const checkData = getCheckData(node!.data, datasets, embeddingProviderModelMap)
const { errorMessage } = nodesExtraData![getNodeMetaType(node!.data) as BlockEnum].checkValid(checkData, t, withFlowType(moreDataForCheckValid, flowType))
const { errorMessage } = nodesExtraData![getNodeCatalogType(node!.data)].checkValid(checkData, t, withFlowType(moreDataForCheckValid, flowType))
if (errorMessage) {
toast.error(`[${node!.data.title}] ${errorMessage}`)

View File

@ -50,6 +50,7 @@ import {
generateNewNode,
genNewNodeTitleFromOld,
getNestedNodePosition,
getNodeCatalogType,
getNodeCustomTypeByNodeDataType,
getNodesConnectedSourceOrTargetHandleIdsMap,
getNodesWithSameDefaultDataType,
@ -747,7 +748,7 @@ export const useNodesInteractions = () => {
return
if (
nodesMetaDataMap?.[currentNode.data.type as BlockEnum]?.metaData
nodesMetaDataMap?.[getNodeCatalogType(currentNode.data)]?.metaData
.isUndeletable
) {
return
@ -1791,7 +1792,7 @@ export const useNodesInteractions = () => {
if (node.type === CUSTOM_NOTE_NODE)
return true
const nodeMeta = nodesMetaDataMap?.[isAgentV2NodeData(node.data) ? BlockEnum.AgentV2 : node.data.type as BlockEnum]
const nodeMeta = nodesMetaDataMap?.[getNodeCatalogType(node.data)]
if (!nodeMeta)
return false
@ -1803,7 +1804,7 @@ export const useNodesInteractions = () => {
if (node.type === CUSTOM_NOTE_NODE)
return {}
const nodeMeta = nodesMetaDataMap?.[isAgentV2NodeData(node.data) ? BlockEnum.AgentV2 : node.data.type as BlockEnum]
const nodeMeta = nodesMetaDataMap?.[getNodeCatalogType(node.data)]
return nodeMeta?.defaultValue
}, [nodesMetaDataMap])

View File

@ -3,9 +3,9 @@ import type { Node } from '@/app/components/workflow/types'
import { useMemo } from 'react'
import { CollectionType } from '@/app/components/tools/types'
import { useHooksStore } from '@/app/components/workflow/hooks-store'
import { isAgentV2NodeData } from '@/app/components/workflow/nodes/agent-v2/types'
import { useStore } from '@/app/components/workflow/store'
import { BlockEnum } from '@/app/components/workflow/types'
import { getNodeCatalogType } from '@/app/components/workflow/utils/node'
import { useGetLanguage } from '@/context/i18n'
import {
useAllBuiltInTools,
@ -33,7 +33,7 @@ export const useNodeMetaData = (node: Node) => {
const dataSourceList = useStore(s => s.dataSourceList)
const availableNodesMetaData = useNodesMetaData()
const { data } = node
const nodeMetaData = availableNodesMetaData.nodesMap?.[isAgentV2NodeData(data) ? BlockEnum.AgentV2 : data.type]
const nodeMetaData = availableNodesMetaData.nodesMap?.[getNodeCatalogType(data)]
const author = useMemo(() => {
if (data.type === BlockEnum.DataSource)
return dataSourceList?.find(dataSource => dataSource.plugin_id === data.plugin_id)?.author

View File

@ -36,6 +36,7 @@ import {
import {
WorkflowRunningStatus,
} from '../types'
import { getNodeCatalogType } from '../utils'
import {
getWorkflowEntryNode,
isWorkflowEntryNode,
@ -360,13 +361,15 @@ export const useWorkflow = () => {
return false
if (sourceNode && targetNode) {
const sourceNodeAvailableNextNodes = getAvailableBlocks(sourceNode.data.type, !!sourceNode.parentId).availableNextBlocks
const targetNodeAvailablePrevNodes = getAvailableBlocks(targetNode.data.type, !!targetNode.parentId).availablePrevBlocks
const sourceNodeCatalogType = getNodeCatalogType(sourceNode.data)
const targetNodeCatalogType = getNodeCatalogType(targetNode.data)
const sourceNodeAvailableNextNodes = getAvailableBlocks(sourceNodeCatalogType, !!sourceNode.parentId).availableNextBlocks
const targetNodeAvailablePrevNodes = getAvailableBlocks(targetNodeCatalogType, !!targetNode.parentId).availablePrevBlocks
if (!sourceNodeAvailableNextNodes.includes(targetNode.data.type))
if (!sourceNodeAvailableNextNodes.includes(targetNodeCatalogType))
return false
if (!targetNodeAvailablePrevNodes.includes(sourceNode.data.type))
if (!targetNodeAvailablePrevNodes.includes(sourceNodeCatalogType))
return false
}

View File

@ -15,6 +15,7 @@ import {
import { useHooksStore } from '@/app/components/workflow/hooks-store'
import useNodes from '@/app/components/workflow/store/workflow/use-nodes'
import { BlockEnum, isTriggerNode } from '@/app/components/workflow/types'
import { getNodeCatalogType } from '@/app/components/workflow/utils'
import { FlowType } from '@/types/common'
type ChangeBlockMenuTriggerProps = {
@ -30,10 +31,11 @@ export function ChangeBlockMenuTrigger({
}: ChangeBlockMenuTriggerProps) {
const { t } = useTranslation()
const { handleNodeChange } = useNodesInteractions()
const nodeCatalogType = getNodeCatalogType(nodeData)
const {
availablePrevBlocks,
availableNextBlocks,
} = useAvailableBlocks(nodeData.type, nodeData.isInIteration || nodeData.isInLoop)
} = useAvailableBlocks(nodeCatalogType, nodeData.isInIteration || nodeData.isInLoop)
const isChatMode = useIsChatMode()
const flowType = useHooksStore(s => s.configsMap?.flowType)
const nodes = useNodes()

View File

@ -18,6 +18,7 @@ import {
useNodesInteractions,
useNodesReadOnly,
} from '@/app/components/workflow/hooks'
import { getNodeCatalogType } from '@/app/components/workflow/utils'
type AddProps = {
nodeId: string
@ -37,7 +38,7 @@ const Add = ({
const [open, setOpen] = useState(false)
const { handleNodeAdd } = useNodesInteractions()
const { nodesReadOnly } = useNodesReadOnly()
const { availableNextBlocks } = useAvailableBlocks(nodeData.type, nodeData.isInIteration || nodeData.isInLoop)
const { availableNextBlocks } = useAvailableBlocks(getNodeCatalogType(nodeData), nodeData.isInIteration || nodeData.isInLoop)
const handleSelect = useCallback<OnSelectBlock>((type, pluginDefaultValue) => {
handleNodeAdd(

View File

@ -18,6 +18,7 @@ import {
useAvailableBlocks,
useNodesInteractions,
} from '@/app/components/workflow/hooks'
import { getNodeCatalogType } from '@/app/components/workflow/utils'
type ChangeItemProps = {
data: CommonNodeType
@ -32,10 +33,11 @@ const ChangeItem = ({
const { t } = useTranslation()
const { handleNodeChange } = useNodesInteractions()
const nodeCatalogType = getNodeCatalogType(data)
const {
availablePrevBlocks,
availableNextBlocks,
} = useAvailableBlocks(data.type, data.isInIteration || data.isInLoop)
} = useAvailableBlocks(nodeCatalogType, data.isInIteration || data.isInLoop)
const handleSelect = useCallback<OnSelectBlock>((type, pluginDefaultValue) => {
handleNodeChange(nodeId, type, sourceHandle, pluginDefaultValue)
@ -59,7 +61,7 @@ const ChangeItem = ({
}}
trigger={renderTrigger}
popupClassName="w-[328px]!"
availableBlocksTypes={intersection(availablePrevBlocks, availableNextBlocks).filter(item => item !== data.type)}
availableBlocksTypes={intersection(availablePrevBlocks, availableNextBlocks).filter(item => item !== nodeCatalogType)}
/>
)
}

View File

@ -28,6 +28,7 @@ import {
BlockEnum,
NodeRunningStatus,
} from '../../../types'
import { getNodeCatalogType } from '../../../utils'
type NodeHandleProps = {
handleId: string
@ -57,7 +58,7 @@ export const NodeTargetHandle = memo(({
const { handleNodeAdd } = useNodesInteractions()
const { getNodesReadOnly } = useNodesReadOnly()
const connected = data._connectedTargetHandleIds?.includes(handleId)
const { availablePrevBlocks } = useAvailableBlocks(data.type, data.isInIteration || data.isInLoop)
const { availablePrevBlocks } = useAvailableBlocks(getNodeCatalogType(data), data.isInIteration || data.isInLoop)
const isConnectable = !!availablePrevBlocks.length
const handleOpenChange = useCallback((v: boolean) => {
@ -147,7 +148,7 @@ export const NodeSourceHandle = memo(({
const workflowStoreApi = useWorkflowStore()
const { handleNodeAdd } = useNodesInteractions()
const { getNodesReadOnly } = useNodesReadOnly()
const { availableNextBlocks } = useAvailableBlocks(data.type, data.isInIteration || data.isInLoop)
const { availableNextBlocks } = useAvailableBlocks(getNodeCatalogType(data), data.isInIteration || data.isInLoop)
const isConnectable = !!availableNextBlocks.length
const isChatMode = useIsChatMode()
const shouldAutoOpen = shouldAutoOpenStartNodeSelector && canAutoOpenStartNodeSelector(data.type, isChatMode)

View File

@ -58,13 +58,13 @@ import { useHooksStore } from '@/app/components/workflow/hooks-store'
import useInspectVarsCrud from '@/app/components/workflow/hooks/use-inspect-vars-crud'
import { NodeActionsDropdown } from '@/app/components/workflow/node-actions-menu'
import Split from '@/app/components/workflow/nodes/_base/components/split'
import { isAgentV2NodeData } from '@/app/components/workflow/nodes/agent-v2/types'
import { useLogs } from '@/app/components/workflow/run/hooks'
import SpecialResultPanel from '@/app/components/workflow/run/special-result-panel'
import { useStore } from '@/app/components/workflow/store'
import { BlockEnum, NodeRunningStatus } from '@/app/components/workflow/types'
import {
canRunBySingle,
getNodeCatalogType,
hasErrorHandleNode,
hasRetryNode,
isSupportCustomRunForm,
@ -210,7 +210,7 @@ const BasePanel: FC<BasePanelProps> = ({
const { handleNodeSelect } = useNodesInteractions()
const { nodesReadOnly } = useNodesReadOnly()
const { availableNextBlocks } = useAvailableBlocks(data.type, data.isInIteration || data.isInLoop)
const { availableNextBlocks } = useAvailableBlocks(getNodeCatalogType(data), data.isInIteration || data.isInLoop)
const toolIcon = useToolIcon(data)
const { saveStateToHistory } = useWorkflowHistory()
@ -230,7 +230,7 @@ const BasePanel: FC<BasePanelProps> = ({
}, [handleNodeDataUpdateWithSyncDraft, id, saveStateToHistory])
const isChildNode = !!(data.isInIteration || data.isInLoop)
const nodeMetaType = isAgentV2NodeData(data) ? BlockEnum.AgentV2 : data.type
const nodeMetaType = getNodeCatalogType(data)
const isSupportSingleRun = canRunBySingle(data.type, isChildNode)
const appDetail = useAppStore(state => state.appDetail)

View File

@ -136,6 +136,7 @@ vi.mock('../hooks', () => ({
vi.mock('../components/agent-orchestrate-drawer-panel', () => ({
AgentOrchestrateDrawerPanel: (props: {
agentId: string
appId?: string
inlineComposerState?: unknown
isInline: boolean
nodeId: string

View File

@ -16,6 +16,7 @@ import { useWorkflowInlineAgentConfigureSync } from '../agent-soul-config'
type AgentOrchestrateDrawerPanelProps = {
agentId: string
appId?: string
inlineComposerState?: WorkflowAgentComposerResponse
isInline: boolean
nodeId: string
@ -32,6 +33,7 @@ export function AgentOrchestrateDrawerPanel(props: AgentOrchestrateDrawerPanelPr
function AgentOrchestrateDrawerPanelContent({
agentId,
appId,
inlineComposerState,
isInline,
nodeId,
@ -76,6 +78,7 @@ function AgentOrchestrateDrawerPanelContent({
return (
<AgentOrchestratePanel
agentId={agentId}
appId={isInline ? appId : undefined}
activeConfigSnapshot={activeConfigSnapshot}
agentSoulConfig={agentSoulConfig as AgentSoulConfig}
agentName={composerState?.agent?.name}

View File

@ -87,6 +87,7 @@ function AgentRosterDrawer({
mode = 'detail',
open,
portalContainerRef,
showAccessIcon = true,
showConsoleLink = true,
showDetailActions = true,
onClose,
@ -96,6 +97,7 @@ function AgentRosterDrawer({
mode?: AgentRosterDrawerMode
open: boolean
portalContainerRef: RefObject<HTMLDivElement | null>
showAccessIcon?: boolean
showConsoleLink?: boolean
showDetailActions?: boolean
onClose: () => void
@ -144,7 +146,7 @@ function AgentRosterDrawer({
<DrawerTitle className={cn('truncate', isSetup ? 'system-xl-semibold text-text-primary' : 'system-sm-medium text-text-secondary')}>
{title}
</DrawerTitle>
{!isSetup && <span aria-hidden className="i-ri-lock-line size-3 shrink-0 text-text-tertiary" />}
{!isSetup && showAccessIcon && <span aria-hidden className="i-ri-lock-line size-3 shrink-0 text-text-tertiary" />}
</div>
{description && (
<p className={cn(isSetup ? 'min-w-full' : 'truncate', 'system-xs-regular text-text-tertiary')}>
@ -341,6 +343,7 @@ export function AgentRosterField({
mode={panelMode}
open={panelOpen}
portalContainerRef={portalContainerRef}
showAccessIcon={!isInlineSetup}
showDetailActions={showPanelDetailActions}
onClose={() => setPanelOpen(false)}
>

View File

@ -33,6 +33,7 @@ export function AgentV2Panel({
const { handleNodeDataUpdate, handleNodeDataUpdateWithSyncDraft } = useNodeDataUpdate()
const openInlineAgentPanelNodeId = useStore(state => state.openInlineAgentPanelNodeId)
const setOpenInlineAgentPanelNodeId = useStore(state => state.setOpenInlineAgentPanelNodeId)
const appId = useStore(state => state.appId)
const drawerPortalContainerRef = useRef<HTMLDivElement>(null)
const declaredOutputs = getAgentV2DeclaredOutputs(inputs)
const rosterAgentId = inputs.agent_binding?.binding_type === 'roster_agent' ? inputs.agent_binding.agent_id : undefined
@ -175,6 +176,7 @@ export function AgentV2Panel({
? (
<AgentOrchestrateDrawerPanel
agentId={displayedAgent.id}
appId={appId}
inlineComposerState={inlineAgentQuery.data}
isInline={isInlineAgentReady}
nodeId={id}

View File

@ -12,11 +12,38 @@ import {
getIterationStartNode,
getLoopStartNode,
getNestedNodePosition,
getNodeCatalogType,
getNodeCustomTypeByNodeDataType,
getTopLeftNodePosition,
hasRetryNode,
} from '../node'
describe('getNodeCatalogType', () => {
it('should use Agent V2 catalog type for graph agent nodes with the Agent V2 discriminator', () => {
expect(getNodeCatalogType({
title: 'Agent',
desc: '',
type: BlockEnum.Agent,
agent_node_kind: 'dify_agent',
version: '2',
} as CommonNodeType)).toBe(BlockEnum.AgentV2)
})
it('should keep the graph node type for regular nodes and legacy Agent nodes', () => {
expect(getNodeCatalogType({
title: 'Code',
desc: '',
type: BlockEnum.Code,
})).toBe(BlockEnum.Code)
expect(getNodeCatalogType({
title: 'Agent',
desc: '',
type: BlockEnum.Agent,
})).toBe(BlockEnum.Agent)
})
})
describe('generateNewNode', () => {
it('should create a basic node with default CUSTOM_NODE type', () => {
const { newNode } = generateNewNode({

View File

@ -1,6 +1,7 @@
import type { IterationNodeType } from '../nodes/iteration/types'
import type { LoopNodeType } from '../nodes/loop/types'
import type {
CommonNodeType,
Node,
} from '../types'
import {
@ -14,12 +15,17 @@ import {
LOOP_CHILDREN_Z_INDEX,
LOOP_NODE_Z_INDEX,
} from '../constants'
import { isAgentV2NodeData } from '../nodes/agent-v2/types'
import { CUSTOM_ITERATION_START_NODE } from '../nodes/iteration-start/constants'
import { CUSTOM_LOOP_START_NODE } from '../nodes/loop-start/constants'
import {
BlockEnum,
} from '../types'
export function getNodeCatalogType(data: CommonNodeType): BlockEnum {
return isAgentV2NodeData(data) ? BlockEnum.AgentV2 : data.type
}
export function generateNewNode({ data, position, id, zIndex, type, ...rest }: Omit<Node, 'id'> & { id?: string }): {
newNode: Node
newIterationStartNode?: Node

View File

@ -22,6 +22,7 @@ describe('agent composer store conversions', () => {
id: 'secret-1',
key: 'OPENAI_API_KEY',
ref: 'credential-1',
value: 'credential-1',
},
],
},
@ -155,6 +156,7 @@ describe('agent composer store conversions', () => {
key: 'OPENAI_API_KEY',
masked: true,
scope: 'secret',
value: 'credential-1',
}),
],
})
@ -229,12 +231,36 @@ describe('agent composer store conversions', () => {
secret_refs: [
expect.objectContaining({
key: 'OPENAI_API_KEY',
ref: 'secret-1',
value: 'credential-1',
}),
],
})
})
it('should hydrate legacy secret refs from ref when value is absent', () => {
const formState = agentSoulConfigToFormState({
env: {
secret_refs: [
{
id: 'secret-1',
key: 'OPENAI_API_KEY',
ref: 'credential-legacy',
},
],
},
})
expect(formState.envVariables).toEqual([
{
id: 'secret-1',
key: 'OPENAI_API_KEY',
masked: true,
scope: 'secret',
value: 'credential-legacy',
},
])
})
it('should keep unauthorized credential type when no-auth tool settings change', () => {
const publishConfig = formStateToAgentSoulConfig({
formState: {
@ -419,7 +445,7 @@ describe('agent composer store conversions', () => {
id: 'valid-secret',
key: 'OPENAI_API_KEY',
name: 'OPENAI_API_KEY',
ref: 'valid-secret',
value: '********',
variable: 'OPENAI_API_KEY',
},
],

View File

@ -78,10 +78,12 @@ const toFileFormState = (config?: AgentSoulConfig): AgentFileNode[] => (
id,
name: file.name ?? id,
icon: toFileIcon(file),
driveKey: file.drive_key ?? undefined,
}]
})
const toFileRefs = (files: AgentFileNode[]) => flattenFileNodes(files).map(file => ({
...(file.driveKey ? { drive_key: file.driveKey } : {}),
id: file.id,
name: file.name,
type: file.icon,
@ -289,10 +291,11 @@ const toCliEnvVariables = (tool: AgentSoulCliToolConfig): EnvVariable[] => [
}),
...(tool.env?.secret_refs ?? []).map((secret): EnvVariable => {
const key = secret.key ?? secret.name ?? secret.variable ?? secret.env_name ?? ''
const value = secret.value ?? secret.ref ?? secret.credential_id ?? ''
return {
id: secret.id ?? secret.ref ?? secret.credential_id ?? key,
key,
value: '••••••••••••',
value,
scope: 'secret',
masked: true,
}
@ -352,7 +355,7 @@ const toCliToolConfigs = (tools: AgentTool[]) => tools.flatMap((tool) => {
id: variable.id,
key: variable.key.trim(),
name: variable.key.trim(),
ref: variable.id,
value: variable.value,
variable: variable.key.trim(),
})),
},
@ -376,10 +379,11 @@ const toEnvVariableFormState = (config?: AgentSoulConfig): EnvVariable[] => [
}),
...(config?.env?.secret_refs ?? []).map((secret): EnvVariable => {
const key = secret.key ?? secret.name ?? secret.variable ?? secret.env_name ?? ''
const value = secret.value ?? secret.ref ?? secret.credential_id ?? ''
return {
id: secret.id ?? secret.ref ?? secret.credential_id ?? key,
key,
value: '••••••••••••',
value,
scope: 'secret',
masked: true,
}
@ -402,7 +406,7 @@ const toEnvConfig = (variables: EnvVariable[]): AgentSoulConfig['env'] => ({
id: variable.id,
key: variable.key.trim(),
name: variable.key.trim(),
ref: variable.id,
value: variable.value,
variable: variable.key.trim(),
})),
})

View File

@ -39,6 +39,7 @@ export type AgentFileNode = {
id: string
name: string
icon: FileTreeIconType
driveKey?: string
children?: AgentFileNode[]
}

View File

@ -38,6 +38,12 @@ vi.mock('@/service/client', () => ({
consoleQuery: {
agent: {
byAgentId: {
get: {
queryKey: ({ input }: { input: { params: { agent_id: string } } }) => [
'agent-detail',
input.params.agent_id,
],
},
composer: {
get: {
queryKey: ({ input }: { input: { params: { agent_id: string } } }) => [
@ -165,6 +171,10 @@ describe('useAgentConfigureSync', () => {
it('should publish only when publishDraft is called explicitly', async () => {
const { queryClient, result } = renderUseAgentConfigureSync()
queryClient.setQueryData(['agent-detail', 'agent-1'], {
active_config_is_published: false,
name: 'Agent',
})
await act(async () => {
await result.current.publishDraft({
@ -198,5 +208,9 @@ describe('useAgentConfigureSync', () => {
},
},
})
expect(queryClient.getQueryData(['agent-detail', 'agent-1'])).toEqual({
active_config_is_published: true,
name: 'Agent',
})
})
})

View File

@ -23,7 +23,7 @@ function renderEmptySections() {
}}
>
<AgentSkills agentId="agent-1" />
<AgentFiles />
<AgentFiles agentId="agent-1" />
<AgentTools />
<AgentKnowledgeRetrieval />
</AgentComposerProvider>

View File

@ -1,7 +1,8 @@
import type { AgentConfigSnapshotSummaryResponse, AgentPublishedReferenceResponse } from '@dify/contracts/api/console/agent/types.gen'
import type { AgentConfigSnapshotSummaryResponse, AgentReferencingWorkflowResponse } from '@dify/contracts/api/console/agent/types.gen'
import type { ComponentProps } from 'react'
import type { Mock } from 'vitest'
import { fireEvent, render, screen, within } from '@testing-library/react'
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
import { act, fireEvent, render, screen, waitFor, within } from '@testing-library/react'
import { createStore, Provider as JotaiProvider } from 'jotai'
import { defaultAgentSoulConfigFormState } from '@/features/agent-v2/agent-composer/form-state'
import { agentComposerDraftAtom, agentComposerOriginalDraftAtom, agentComposerPublishedDraftAtom } from '@/features/agent-v2/agent-composer/store'
@ -18,6 +19,9 @@ const hotkeyRegistrations = vi.hoisted(() => new Map<string, {
const mockFormatForDisplay = vi.hoisted(() => vi.fn((hotkey: string) => `display:${hotkey}`))
const mockFormatTimeFromNow = vi.hoisted(() => vi.fn(() => 'just now'))
const workflowReferences = vi.hoisted(() => ({
data: [] as AgentReferencingWorkflowResponse[],
}))
vi.mock('@tanstack/react-hotkeys', async (importOriginal) => {
const actual = await importOriginal<typeof import('@tanstack/react-hotkeys')>()
@ -36,8 +40,28 @@ vi.mock('@/hooks/use-format-time-from-now', () => ({
}),
}))
vi.mock('@/service/client', () => ({
consoleQuery: {
agent: {
byAgentId: {
referencingWorkflows: {
get: {
queryOptions: ({ input }: { input: { params: { agent_id: string } } }) => ({
queryKey: ['agent-referencing-workflows', input],
queryFn: async () => ({
data: workflowReferences.data,
}),
}),
},
},
},
},
},
}))
vi.mock('@langgenius/dify-ui/popover', async () => {
const React = await import('react')
const ReactDOM = await import('react-dom')
const PopoverContext = React.createContext<{
open: boolean
onOpenChange?: (open: boolean) => void
@ -68,7 +92,7 @@ vi.mock('@langgenius/dify-ui/popover', async () => {
if (!context.open)
return null
return <div data-testid="publish-impact-popover">{children}</div>
return ReactDOM.createPortal(<div data-testid="publish-impact-popover">{children}</div>, document.body)
},
PopoverTrigger: ({
render: trigger,
@ -96,60 +120,73 @@ const originalDraftWithFile = {
],
} satisfies typeof defaultAgentSoulConfigFormState
const publishedReferences: AgentPublishedReferenceResponse[] = [
const publishedReferences: AgentReferencingWorkflowResponse[] = [
{
app_id: 'app-python',
app_mode: 'workflow',
app_name: 'Python bug fixer',
workflow_id: 'workflow-python',
workflow_version: '1',
node_ids: ['node-python'],
},
{
app_id: 'app-translation',
app_icon: 'T',
app_icon_background: '#E0F2FE',
app_icon_type: 'emoji',
app_mode: 'workflow',
app_name: 'Translation Workflow',
workflow_id: 'workflow-translation',
workflow_version: '1',
node_ids: ['node-translation'],
},
]
function renderPublishBar({
activeConfigIsPublished,
activeConfigSnapshot,
draftSavedAt,
isPublishing,
onPublish = vi.fn<PublishHandler>(),
prompt = '',
publishedReferenceCount,
publishedReferences,
setupStore,
usedByAppReferences = [],
}: {
activeConfigIsPublished?: boolean
activeConfigSnapshot?: AgentConfigSnapshotSummaryResponse | null
draftSavedAt?: number
isPublishing?: boolean
onPublish?: PublishMock
prompt?: string
publishedReferenceCount?: number
publishedReferences?: AgentPublishedReferenceResponse[]
setupStore?: (store: ReturnType<typeof createStore>) => void
usedByAppReferences?: AgentReferencingWorkflowResponse[]
} = {}) {
workflowReferences.data = usedByAppReferences
const queryClient = new QueryClient({
defaultOptions: {
queries: { retry: false },
mutations: { retry: false },
},
})
const store = createStore()
store.set(agentComposerPromptAtom, prompt)
setupStore?.(store)
render(
<JotaiProvider store={store}>
<AgentConfigurePublishBar
agentId="agent-1"
activeConfigSnapshot={activeConfigSnapshot}
draftSavedAt={draftSavedAt}
agentName="Iris"
isPublishing={isPublishing}
publishedReferenceCount={publishedReferenceCount}
publishedReferences={publishedReferences}
onPublish={onPublish}
onOpenVersions={vi.fn()}
/>
</JotaiProvider>,
<QueryClientProvider client={queryClient}>
<JotaiProvider store={store}>
<AgentConfigurePublishBar
agentId="agent-1"
activeConfigIsPublished={activeConfigIsPublished}
activeConfigSnapshot={activeConfigSnapshot}
draftSavedAt={draftSavedAt}
agentName="Iris"
isPublishing={isPublishing}
onPublish={onPublish}
onOpenVersions={vi.fn()}
/>
</JotaiProvider>
</QueryClientProvider>,
)
return {
@ -161,6 +198,7 @@ describe('AgentConfigurePublishBar', () => {
beforeEach(() => {
vi.clearAllMocks()
hotkeyRegistrations.clear()
workflowReferences.data = []
vi.spyOn(console, 'log').mockImplementation(() => {})
})
@ -191,7 +229,10 @@ describe('AgentConfigurePublishBar', () => {
})
it('should render published state from the active snapshot and disable publish logic', () => {
const { onPublish } = renderPublishBar({ activeConfigSnapshot })
const { onPublish } = renderPublishBar({
activeConfigIsPublished: true,
activeConfigSnapshot,
})
expect(screen.getByText('agentV2.agentDetail.configure.publishBar.upToDate')).toBeInTheDocument()
expect(screen.getByText(/agentV2\.agentDetail\.configure\.publishBar\.publishedAt/)).toBeInTheDocument()
@ -207,7 +248,28 @@ describe('AgentConfigurePublishBar', () => {
expect(onPublish).not.toHaveBeenCalled()
})
it('should publish the current draft payload from the unpublished changes state', () => {
it('should initialize unpublished state when active config is not published', async () => {
const { onPublish } = renderPublishBar({
activeConfigIsPublished: false,
activeConfigSnapshot,
})
expect(screen.getByText('agentV2.agentDetail.configure.publishBar.unpublishedChanges')).toBeInTheDocument()
expect(screen.getByRole('button', { name: /agentV2\.agentDetail\.configure\.publishBar\.publishUpdate/ })).toBeInTheDocument()
expect(hotkeyRegistrations.get('Mod+Shift+P')?.options).toEqual(
expect.objectContaining({ enabled: true, ignoreInputs: false }),
)
fireEvent.click(screen.getByRole('button', { name: /agentV2\.agentDetail\.configure\.publishBar\.publishUpdate/ }))
await waitFor(() => {
expect(onPublish).toHaveBeenCalledWith(expect.objectContaining({
agent_id: 'agent-1',
}))
})
})
it('should publish the current draft payload from the unpublished changes state', async () => {
const { onPublish } = renderPublishBar({
activeConfigSnapshot,
prompt: 'Updated system prompt',
@ -217,14 +279,16 @@ describe('AgentConfigurePublishBar', () => {
fireEvent.click(screen.getByRole('button', { name: /agentV2\.agentDetail\.configure\.publishBar\.publishUpdate/ }))
expect(onPublish).toHaveBeenCalledWith(expect.objectContaining({
agent_id: 'agent-1',
config_snapshot: expect.objectContaining({
prompt: expect.objectContaining({
system_prompt: 'Updated system prompt',
await waitFor(() => {
expect(onPublish).toHaveBeenCalledWith(expect.objectContaining({
agent_id: 'agent-1',
config_snapshot: expect.objectContaining({
prompt: expect.objectContaining({
system_prompt: 'Updated system prompt',
}),
}),
}),
}))
}))
})
})
it('should mark non-prompt draft changes as unpublished', () => {
@ -277,20 +341,27 @@ describe('AgentConfigurePublishBar', () => {
)
})
it('should show affected workflow references when clicking a publishable agent in use', () => {
it('should show affected workflow references when clicking a publishable agent in use', async () => {
const { onPublish } = renderPublishBar({
activeConfigSnapshot,
prompt: 'Updated system prompt',
publishedReferenceCount: 2,
publishedReferences,
usedByAppReferences: publishedReferences,
})
expect(screen.queryByTestId('publish-impact-popover')).not.toBeInTheDocument()
const publishBar = screen.getByText('agentV2.agentDetail.configure.publishBar.unpublishedChanges').closest('[aria-hidden]')
expect(publishBar).toHaveAttribute('aria-hidden', 'false')
fireEvent.click(screen.getByRole('button', { name: /agentV2\.agentDetail\.configure\.publishBar\.publishUpdate/ }))
expect(onPublish).not.toHaveBeenCalled()
expect(screen.getByTestId('publish-impact-popover')).toBeInTheDocument()
const impactPopover = await screen.findByTestId('publish-impact-popover')
expect(impactPopover).toBeInTheDocument()
expect(publishBar).toHaveAttribute('aria-hidden', 'false')
await waitFor(() => {
expect(publishBar).toHaveAttribute('aria-hidden', 'true')
expect(publishBar).toHaveClass('opacity-0')
})
expect(screen.getByText(/agentV2\.agentDetail\.configure\.publishImpact\.title/)).toBeInTheDocument()
expect(screen.getByText(/agentV2\.agentDetail\.configure\.publishImpact\.descriptionPrefix/)).toBeInTheDocument()
expect(screen.getByText(/agentV2\.agentDetail\.configure\.publishImpact\.workflowCount/)).toBeInTheDocument()
@ -298,18 +369,20 @@ describe('AgentConfigurePublishBar', () => {
expect(screen.getByText('Translation Workflow')).toBeInTheDocument()
expect(screen.getByRole('link', { name: 'Python bug fixer' })).toHaveAttribute('target', '_blank')
expect(screen.getByRole('link', { name: 'Python bug fixer' })).toHaveAttribute('rel', 'noopener noreferrer')
expect(within(impactPopover).getByText('display:Mod')).toBeInTheDocument()
expect(within(impactPopover).getByText('display:Shift')).toBeInTheDocument()
expect(within(impactPopover).getByText('display:P')).toBeInTheDocument()
})
it('should publish from the affected workflow popover action', () => {
it('should publish from the affected workflow popover action', async () => {
const { onPublish } = renderPublishBar({
activeConfigSnapshot,
prompt: 'Updated system prompt',
publishedReferenceCount: 2,
publishedReferences,
usedByAppReferences: publishedReferences,
})
fireEvent.click(screen.getByRole('button', { name: /agentV2\.agentDetail\.configure\.publishBar\.publishUpdate/ }))
fireEvent.click(within(screen.getByTestId('publish-impact-popover')).getByRole('button', {
fireEvent.click(within(await screen.findByTestId('publish-impact-popover')).getByRole('button', {
name: /agentV2\.agentDetail\.configure\.publishBar\.publishUpdate/,
}))
@ -317,4 +390,28 @@ describe('AgentConfigurePublishBar', () => {
agent_id: 'agent-1',
}))
})
it('should open the affected workflow popover from the publish shortcut before publishing', async () => {
const { onPublish } = renderPublishBar({
activeConfigSnapshot,
prompt: 'Updated system prompt',
usedByAppReferences: publishedReferences,
})
const publishShortcut = hotkeyRegistrations.get('Mod+Shift+P')
await act(async () => {
await publishShortcut?.callback({ preventDefault: vi.fn() })
})
expect(onPublish).not.toHaveBeenCalled()
expect(await screen.findByTestId('publish-impact-popover')).toBeInTheDocument()
await act(async () => {
await hotkeyRegistrations.get('Mod+Shift+P')?.callback({ preventDefault: vi.fn() })
})
expect(onPublish).toHaveBeenCalledWith(expect.objectContaining({
agent_id: 'agent-1',
}))
})
})

View File

@ -32,12 +32,12 @@ export function ConfigureSectionConfigurableItem({
</span>
</div>
{!readOnly && (
<div className="hidden shrink-0 items-center gap-1 group-focus-within:flex group-hover:flex">
<div className="pointer-events-none flex shrink-0 items-center gap-1 opacity-0 transition-opacity group-focus-within:pointer-events-auto group-focus-within:opacity-100 group-hover:pointer-events-auto group-hover:opacity-100">
<button
type="button"
aria-label={editAriaLabel}
onClick={onEdit}
className="flex size-6 items-center justify-center rounded-md text-text-tertiary hover:bg-state-base-hover hover:text-text-secondary focus-visible:ring-2 focus-visible:ring-state-accent-solid focus-visible:outline-hidden"
className="flex size-6 items-center justify-center rounded-md text-text-tertiary hover:bg-state-base-hover hover:text-text-secondary focus-visible:bg-state-base-hover focus-visible:text-text-secondary focus-visible:ring-2 focus-visible:ring-state-accent-solid focus-visible:outline-hidden"
>
<span aria-hidden className="i-ri-equalizer-2-line size-4" />
</button>
@ -45,14 +45,14 @@ export function ConfigureSectionConfigurableItem({
type="button"
aria-label={removeAriaLabel}
onClick={onRemove}
className="flex size-6 items-center justify-center rounded-md text-text-tertiary hover:bg-state-destructive-hover hover:text-text-destructive focus-visible:ring-2 focus-visible:ring-state-accent-solid focus-visible:outline-hidden"
className="flex size-6 items-center justify-center rounded-md text-text-tertiary hover:bg-state-destructive-hover hover:text-text-destructive focus-visible:bg-state-destructive-hover focus-visible:text-text-destructive focus-visible:ring-2 focus-visible:ring-state-accent-solid focus-visible:outline-hidden"
>
<span aria-hidden className="i-ri-delete-bin-line size-4" />
</button>
</div>
)}
{hasBadge && (
<span className="shrink-0 rounded-[5px] border border-divider-deep bg-components-badge-bg-dimm px-1 py-0.5 system-2xs-medium-uppercase text-text-tertiary group-focus-within:hidden group-hover:hidden">
<span className="shrink-0 rounded-[5px] border border-divider-deep bg-components-badge-bg-dimm px-1 py-0.5 system-2xs-medium-uppercase text-text-tertiary transition-opacity group-focus-within:opacity-0 group-hover:opacity-0">
{badge}
</span>
)}

View File

@ -0,0 +1,34 @@
import { describe, expect, it } from 'vitest'
import { getDriveFileIconType, getFileIconType } from '../file-icon'
describe('agent file icon helpers', () => {
it('should infer supported icons for uploaded drive file pointer kinds', () => {
expect(getDriveFileIconType({
fileKind: 'upload_file',
fileName: 'report.md',
mimeType: 'text/markdown',
})).toBe('markdown')
expect(getDriveFileIconType({
fileKind: 'tool_file',
fileName: 'image.png',
mimeType: 'image/png',
})).toBe('image')
})
it('should keep supported drive file kinds and normalize directories', () => {
expect(getDriveFileIconType({
fileKind: 'directory',
fileName: 'files',
})).toBe('folder')
expect(getDriveFileIconType({
fileKind: 'pdf',
fileName: 'guide',
})).toBe('pdf')
})
it('should infer icons from file extension when mime type is not enough', () => {
expect(getFileIconType('data.csv')).toBe('table')
expect(getFileIconType('archive.zip')).toBe('archive')
expect(getFileIconType('script.ts')).toBe('code')
})
})

View File

@ -1,5 +1,6 @@
import type { AgentSoulConfigFormState } from '@/features/agent-v2/agent-composer/form-state'
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
import { fireEvent, render, screen, within } from '@testing-library/react'
import { fireEvent, render, screen, waitFor, within } from '@testing-library/react'
import { beforeEach, describe, expect, it, vi } from 'vitest'
import { defaultAgentSoulConfigFormState } from '@/features/agent-v2/agent-composer/form-state'
import { AgentComposerProvider } from '@/features/agent-v2/agent-composer/provider'
@ -7,20 +8,87 @@ import { AgentOrchestrateReadOnlyContext } from '../../read-only-context'
import { AgentFiles } from '../index'
const mocks = vi.hoisted(() => ({
filePreviewQueryOptions: vi.fn(),
agentDriveFilesQueryOptions: vi.fn(),
agentFileCommitMutationFn: vi.fn(),
agentFileDeleteMutationFn: vi.fn(),
agentFileDeleteMutationOptions: vi.fn(),
agentFileDownloadQueryOptions: vi.fn(),
agentFilePreviewQueryOptions: vi.fn(),
agentFileCommitMutationOptions: vi.fn(),
workflowAgentDriveFilesQueryOptions: vi.fn(),
workflowAgentFileCommitMutationFn: vi.fn(),
workflowAgentFileDeleteMutationFn: vi.fn(),
workflowAgentFileDeleteMutationOptions: vi.fn(),
workflowAgentFileDownloadQueryOptions: vi.fn(),
workflowAgentFilePreviewQueryOptions: vi.fn(),
workflowAgentFileCommitMutationOptions: vi.fn(),
uploadFileMutationFn: vi.fn(),
uploadFileMutationOptions: vi.fn(),
}))
vi.mock('@/service/client', () => ({
consoleQuery: {
files: {
byFileId: {
preview: {
get: {
queryOptions: mocks.filePreviewQueryOptions,
agent: {
byAgentId: {
drive: {
files: {
get: {
queryOptions: mocks.agentDriveFilesQueryOptions,
},
download: {
get: {
queryOptions: mocks.agentFileDownloadQueryOptions,
},
},
preview: {
get: {
queryOptions: mocks.agentFilePreviewQueryOptions,
},
},
},
},
files: {
delete: {
mutationOptions: mocks.agentFileDeleteMutationOptions,
},
post: {
mutationOptions: mocks.agentFileCommitMutationOptions,
},
},
},
},
apps: {
byAppId: {
agent: {
drive: {
files: {
get: {
queryOptions: mocks.workflowAgentDriveFilesQueryOptions,
},
download: {
get: {
queryOptions: mocks.workflowAgentFileDownloadQueryOptions,
},
},
preview: {
get: {
queryOptions: mocks.workflowAgentFilePreviewQueryOptions,
},
},
},
},
files: {
delete: {
mutationOptions: mocks.workflowAgentFileDeleteMutationOptions,
},
post: {
mutationOptions: mocks.workflowAgentFileCommitMutationOptions,
},
},
},
},
},
files: {
upload: {
post: {
mutationOptions: mocks.uploadFileMutationOptions,
@ -37,16 +105,36 @@ const agentFilesDraft = {
id: 'preview-image',
name: 'agent-roster-skill-detail-dialog-preview-image.png',
icon: 'image',
driveKey: 'files/agent-roster-skill-detail-dialog-preview-image.png',
},
{
id: 'brief',
name: 'brief.md',
icon: 'markdown',
driveKey: 'files/brief.md',
},
],
} satisfies typeof defaultAgentSoulConfigFormState
} satisfies AgentSoulConfigFormState
function renderAgentFiles() {
const agentSkillFilesDraft = {
...defaultAgentSoulConfigFormState,
files: [
{
id: 'script',
name: 'run.py',
icon: 'file',
driveKey: 'files/run.py',
},
{
id: 'skill-md',
name: 'SKILL.md',
icon: 'markdown',
driveKey: 'files/SKILL.md',
},
],
} satisfies AgentSoulConfigFormState
function renderAgentFiles(initialDraft: AgentSoulConfigFormState = agentFilesDraft) {
const queryClient = new QueryClient({
defaultOptions: {
queries: {
@ -57,8 +145,8 @@ function renderAgentFiles() {
return render(
<QueryClientProvider client={queryClient}>
<AgentComposerProvider initialDraft={agentFilesDraft}>
<AgentFiles />
<AgentComposerProvider initialDraft={initialDraft}>
<AgentFiles agentId="agent-1" />
</AgentComposerProvider>
</QueryClientProvider>,
)
@ -77,7 +165,7 @@ function renderReadonlyAgentFiles() {
<QueryClientProvider client={queryClient}>
<AgentComposerProvider initialDraft={agentFilesDraft}>
<AgentOrchestrateReadOnlyContext value>
<AgentFiles />
<AgentFiles agentId="agent-1" />
</AgentOrchestrateReadOnlyContext>
</AgentComposerProvider>
</QueryClientProvider>,
@ -87,38 +175,272 @@ function renderReadonlyAgentFiles() {
describe('AgentFiles', () => {
beforeEach(() => {
vi.clearAllMocks()
mocks.filePreviewQueryOptions.mockImplementation(({ input }) => ({
queryKey: ['file-preview', input],
mocks.agentDriveFilesQueryOptions.mockImplementation(({ input }) => ({
queryKey: ['agent-drive-files', input],
queryFn: () => new Promise(() => {}),
}))
mocks.workflowAgentDriveFilesQueryOptions.mockImplementation(({ input }) => ({
queryKey: ['workflow-agent-drive-files', input],
queryFn: () => new Promise(() => {}),
}))
mocks.agentFilePreviewQueryOptions.mockImplementation(({ input }) => ({
queryKey: ['agent-file-preview', input],
queryFn: async () => ({
content: `Preview content for ${input.params.file_id}`,
binary: false,
text: `Preview content for ${input.query.key}`,
}),
}))
mocks.workflowAgentFilePreviewQueryOptions.mockImplementation(({ input }) => ({
queryKey: ['workflow-agent-file-preview', input],
queryFn: async () => ({
binary: false,
text: `Preview content for ${input.query.key}`,
}),
}))
mocks.agentFileDownloadQueryOptions.mockImplementation(({ input }) => ({
queryKey: ['agent-file-download', input],
queryFn: async () => ({
url: `https://signed.example/${input.query.key}`,
}),
}))
mocks.workflowAgentFileDownloadQueryOptions.mockImplementation(({ input }) => ({
queryKey: ['workflow-agent-file-download', input],
queryFn: async () => ({
url: `https://signed.example/${input.query.key}`,
}),
}))
mocks.uploadFileMutationOptions.mockReturnValue({
mutationFn: vi.fn(),
mutationFn: mocks.uploadFileMutationFn.mockResolvedValue({
id: 'upload-file-1',
name: 'uploaded.md',
mime_type: 'text/markdown',
}),
mutationKey: ['upload-file'],
})
mocks.agentFileCommitMutationOptions.mockReturnValue({
mutationFn: mocks.agentFileCommitMutationFn.mockResolvedValue({
file: {
drive_key: 'files/uploaded.md',
file_id: 'drive-file-1',
mime_type: 'text/markdown',
name: 'uploaded.md',
},
}),
mutationKey: ['commit-agent-file'],
})
mocks.workflowAgentFileCommitMutationOptions.mockReturnValue({
mutationFn: mocks.workflowAgentFileCommitMutationFn.mockResolvedValue({
file: {
drive_key: 'files/uploaded.md',
file_id: 'drive-file-1',
mime_type: 'text/markdown',
name: 'uploaded.md',
},
}),
mutationKey: ['commit-workflow-agent-file'],
})
mocks.agentFileDeleteMutationOptions.mockReturnValue({
mutationFn: mocks.agentFileDeleteMutationFn.mockResolvedValue({ result: 'success' }),
mutationKey: ['delete-agent-file'],
})
mocks.workflowAgentFileDeleteMutationOptions.mockReturnValue({
mutationFn: mocks.workflowAgentFileDeleteMutationFn.mockResolvedValue({ result: 'success' }),
mutationKey: ['delete-workflow-agent-file'],
})
})
it('should list Agent App drive files under the files prefix', () => {
renderAgentFiles()
expect(mocks.agentDriveFilesQueryOptions).toHaveBeenCalledWith({
input: {
params: {
agent_id: 'agent-1',
},
query: {
prefix: 'files/',
},
},
})
})
it('should keep the file preview trigger focus ring inside the row bounds', () => {
renderAgentFiles()
expect(screen.getByRole('button', { name: 'brief.md' })).toHaveClass(
'focus-visible:ring-2',
'focus-visible:ring-state-accent-solid',
'focus-visible:ring-inset',
)
})
it('should open the shared detail dialog with the full file tree when the file row is clicked', async () => {
renderAgentFiles()
fireEvent.click(screen.getByRole('button', {
name: 'agent-roster-skill-detail-dialog-preview-image.png',
name: 'brief.md',
}))
const dialog = screen.getByRole('dialog')
expect(dialog).toBeInTheDocument()
expect(within(dialog).getAllByText('agent-roster-skill-detail-dialog-preview-image.png')).toHaveLength(2)
expect(within(dialog).getByText('brief.md')).toBeInTheDocument()
expect(mocks.filePreviewQueryOptions).toHaveBeenCalledWith({
expect(within(dialog).getByText('agent-roster-skill-detail-dialog-preview-image.png')).toBeInTheDocument()
expect(within(dialog).getAllByText('brief.md')).toHaveLength(2)
expect(mocks.agentFilePreviewQueryOptions).toHaveBeenCalledWith({
input: {
params: {
file_id: 'preview-image',
agent_id: 'agent-1',
},
query: {
key: 'files/brief.md',
},
},
})
expect(await within(dialog).findByText('Preview content for preview-image')).toBeInTheDocument()
expect(await within(dialog).findByText('Preview content for files/brief.md')).toBeInTheDocument()
})
it('should preview the clicked file when SKILL.md also exists', async () => {
renderAgentFiles(agentSkillFilesDraft)
fireEvent.click(screen.getByRole('button', {
name: 'run.py',
}))
const dialog = screen.getByRole('dialog')
expect(await within(dialog).findByText('Preview content for files/run.py')).toBeInTheDocument()
expect(mocks.agentFilePreviewQueryOptions).toHaveBeenCalledWith({
input: {
params: {
agent_id: 'agent-1',
},
query: {
key: 'files/run.py',
},
},
})
})
it('should preview the selected file from the detail file tree', async () => {
renderAgentFiles(agentSkillFilesDraft)
fireEvent.click(screen.getByRole('button', {
name: 'run.py',
}))
const dialog = screen.getByRole('dialog')
const skillFile = within(dialog).getByRole('button', { name: 'SKILL.md' })
fireEvent.click(skillFile)
expect(await within(dialog).findByText('Preview content for files/SKILL.md')).toBeInTheDocument()
const scriptFile = within(dialog).getAllByRole('button', { name: 'run.py' }).at(-1)
expect(scriptFile).toBeDefined()
fireEvent.click(scriptFile!)
expect(await within(dialog).findByText('Preview content for files/run.py')).toBeInTheDocument()
expect(mocks.agentFilePreviewQueryOptions).toHaveBeenCalledWith({
input: {
params: {
agent_id: 'agent-1',
},
query: {
key: 'files/run.py',
},
},
})
})
it('should render image files directly from the drive download URL without a download link', async () => {
mocks.agentFilePreviewQueryOptions.mockImplementation(({ input }) => ({
queryKey: ['agent-file-preview', input],
queryFn: async () => ({
binary: false,
key: input.query.key,
size: 12345,
text: 'image preview should not render as text',
truncated: false,
}),
}))
renderAgentFiles()
fireEvent.click(screen.getByRole('button', {
name: 'agent-roster-skill-detail-dialog-preview-image.png',
}))
const image = await screen.findByRole('img', {
name: 'agent-roster-skill-detail-dialog-preview-image.png',
})
expect(image).toHaveAttribute('src', 'https://signed.example/files/agent-roster-skill-detail-dialog-preview-image.png')
expect(screen.queryByRole('link', { name: /common\.operation\.download/ })).not.toBeInTheDocument()
expect(screen.queryByText('image preview should not render as text')).not.toBeInTheDocument()
expect(mocks.agentFileDownloadQueryOptions).toHaveBeenCalledWith({
input: {
params: {
agent_id: 'agent-1',
},
query: {
key: 'files/agent-roster-skill-detail-dialog-preview-image.png',
},
},
})
})
it('should render a download link for binary non-image files', async () => {
mocks.agentFilePreviewQueryOptions.mockImplementation(({ input }) => ({
queryKey: ['agent-file-preview', input],
queryFn: async () => ({
binary: true,
key: input.query.key,
size: 12345,
text: null,
truncated: false,
}),
}))
renderAgentFiles()
fireEvent.click(screen.getByRole('button', {
name: 'brief.md',
}))
const link = await screen.findByRole('link', { name: 'common.operation.download' })
expect(screen.getByText('agentV2.agentDetail.configure.files.preview.unsupported')).toBeInTheDocument()
expect(link).toHaveAttribute('href', 'https://signed.example/files/brief.md')
expect(screen.queryByText('Preview content for files/brief.md')).not.toBeInTheDocument()
})
it('should commit an uploaded file to the Agent App drive before adding it to the composer draft', async () => {
renderAgentFiles({
...defaultAgentSoulConfigFormState,
files: [],
})
fireEvent.click(screen.getByRole('button', { name: 'agentV2.agentDetail.configure.files.add' }))
const input = document.querySelector('input[type="file"]')
expect(input).toBeInstanceOf(HTMLInputElement)
fireEvent.change(input!, {
target: {
files: [new File(['# Uploaded'], 'uploaded.md', { type: 'text/markdown' })],
},
})
fireEvent.click(screen.getByRole('button', { name: 'agentV2.agentDetail.configure.files.upload.action' }))
await waitFor(() => {
expect(mocks.agentFileCommitMutationFn).toHaveBeenCalledWith(
{
params: {
agent_id: 'agent-1',
},
body: {
upload_file_id: 'upload-file-1',
},
},
expect.anything(),
)
})
})
// File rows expose a hover/focus remove action that updates the composer draft.

View File

@ -0,0 +1,7 @@
export type AgentFileApiContext = {
agentId: string
workflow?: {
appId: string
nodeId: string
}
}

View File

@ -0,0 +1,81 @@
import type { FileTreeIconType } from '@langgenius/dify-ui/file-tree'
const codeFileExtensions = new Set([
'css',
'go',
'html',
'js',
'jsx',
'py',
'rb',
'rs',
'scss',
'sh',
'ts',
'tsx',
'vue',
'yaml',
'yml',
])
const tableFileExtensions = new Set(['csv', 'xls', 'xlsx'])
const archiveFileExtensions = new Set(['7z', 'gz', 'rar', 'tar', 'zip'])
const driveFileIconTypes = new Set<FileTreeIconType>([
'archive',
'code',
'database',
'file',
'folder',
'image',
'json',
'markdown',
'pdf',
'table',
'text',
])
function getFileExtension(fileName: string) {
return fileName.split('.').pop()?.toLowerCase() ?? ''
}
export function getFileIconType(fileName: string, mimeType?: string | null): FileTreeIconType {
const extension = getFileExtension(fileName)
if (mimeType?.startsWith('image/'))
return 'image'
if (mimeType === 'application/pdf' || extension === 'pdf')
return 'pdf'
if (extension === 'md' || extension === 'markdown' || extension === 'mdx')
return 'markdown'
if (extension === 'json')
return 'json'
if (tableFileExtensions.has(extension))
return 'table'
if (archiveFileExtensions.has(extension))
return 'archive'
if (codeFileExtensions.has(extension))
return 'code'
if (mimeType?.startsWith('text/'))
return 'text'
return 'file'
}
export function getDriveFileIconType({
fileKind,
fileName,
mimeType,
}: {
fileKind?: string | null
fileName: string
mimeType?: string | null
}): FileTreeIconType {
const normalizedFileKind = fileKind?.toLowerCase()
if (normalizedFileKind === 'directory')
return 'folder'
if (normalizedFileKind && driveFileIconTypes.has(normalizedFileKind as FileTreeIconType))
return normalizedFileKind as FileTreeIconType
return getFileIconType(fileName, mimeType)
}

View File

@ -2,16 +2,18 @@
import type { ReactNode } from 'react'
import type { AgentOrchestrateAddActionOptions } from '../add-actions-context'
import type { AgentFileApiContext } from './api-context'
import type { AgentFileNode } from '@/features/agent-v2/agent-composer/form-state'
import {
Dialog,
DialogTrigger,
} from '@langgenius/dify-ui/dialog'
import {
FileTreeGuide,
} from '@langgenius/dify-ui/file-tree'
import { useQuery } from '@tanstack/react-query'
import { useMutation, useQuery } from '@tanstack/react-query'
import { useAtom } from 'jotai'
import { useCallback, useRef, useState } from 'react'
import { useCallback, useMemo, useRef, useState } from 'react'
import { useTranslation } from 'react-i18next'
import { agentComposerFilesAtom } from '@/features/agent-v2/agent-composer/store-modules/files'
import { consoleQuery } from '@/service/client'
@ -21,6 +23,7 @@ import { ConfigureSectionEmpty } from '../common/empty'
import { ConfigureSection } from '../common/section'
import { useAgentOrchestrateReadOnly } from '../read-only-context'
import { AgentSkillDetailDialog } from '../skills/detail-dialog'
import { getDriveFileIconType } from './file-icon'
import { AgentFileTree } from './tree'
import { AgentFileUploadDialog } from './upload-dialog'
@ -31,11 +34,47 @@ const removeFileNode = (files: AgentFileNode[], fileId: string): AgentFileNode[]
children: file.children ? removeFileNode(file.children, fileId) : undefined,
}))
const FILES_DRIVE_PREFIX = 'files/'
const getAgentFilePreviewKey = (file: AgentFileNode) => file.driveKey ?? file.id
const findAgentFileNode = (files: AgentFileNode[], fileId: string): AgentFileNode | undefined => {
for (const file of files) {
if (file.id === fileId)
return file
const child = file.children ? findAgentFileNode(file.children, fileId) : undefined
if (child)
return child
}
}
const getAgentDriveFileName = (key: string) => {
const normalizedKey = key.endsWith('/') ? key.slice(0, -1) : key
return normalizedKey.split('/').pop() || normalizedKey
}
const toAgentFileNodeFromDriveItem = (item: {
file_kind: string
key: string
mime_type?: string | null
}): AgentFileNode => ({
id: item.key,
name: getAgentDriveFileName(item.key),
icon: getDriveFileIconType({
fileKind: item.file_kind,
fileName: getAgentDriveFileName(item.key),
mimeType: item.mime_type,
}),
driveKey: item.key,
})
function AgentFileItem({
children,
depth,
file,
files,
apiContext,
onRemove,
selected,
}: {
@ -43,35 +82,96 @@ function AgentFileItem({
depth: number
file: AgentFileNode
files: AgentFileNode[]
apiContext: AgentFileApiContext
onRemove: (fileId: string) => void
selected: boolean
}) {
const { t } = useTranslation('agentV2')
const readOnly = useAgentOrchestrateReadOnly()
const [isPreviewOpen, setIsPreviewOpen] = useState(false)
const previewQuery = useQuery({
...consoleQuery.files.byFileId.preview.get.queryOptions({
const [selectedFileId, setSelectedFileId] = useState<string>()
const selectedFile = selectedFileId ? findAgentFileNode(files, selectedFileId) : undefined
const previewFileId = getAgentFilePreviewKey(selectedFile ?? file)
const agentPreviewQuery = useQuery({
...consoleQuery.agent.byAgentId.drive.files.preview.get.queryOptions({
input: {
params: {
file_id: file.id,
agent_id: apiContext.agentId,
},
query: {
key: previewFileId ?? '',
},
},
}),
enabled: isPreviewOpen,
enabled: isPreviewOpen && !!previewFileId && !apiContext.workflow,
})
const workflowPreviewQuery = useQuery({
...consoleQuery.apps.byAppId.agent.drive.files.preview.get.queryOptions({
input: {
params: {
app_id: apiContext.workflow?.appId ?? '',
},
query: {
key: previewFileId ?? '',
node_id: apiContext.workflow?.nodeId,
},
},
}),
enabled: isPreviewOpen && !!previewFileId && !!apiContext.workflow,
})
const previewQuery = apiContext.workflow ? workflowPreviewQuery : agentPreviewQuery
const selectedPreviewFile = selectedFile ?? file
const isImagePreviewFile = selectedPreviewFile.icon === 'image'
const shouldDownloadPreviewFile = isPreviewOpen && !!previewFileId && (isImagePreviewFile || !!previewQuery.data?.binary)
const agentDownloadQuery = useQuery({
...consoleQuery.agent.byAgentId.drive.files.download.get.queryOptions({
input: {
params: {
agent_id: apiContext.agentId,
},
query: {
key: previewFileId ?? '',
},
},
}),
enabled: shouldDownloadPreviewFile && !apiContext.workflow,
})
const workflowDownloadQuery = useQuery({
...consoleQuery.apps.byAppId.agent.drive.files.download.get.queryOptions({
input: {
params: {
app_id: apiContext.workflow?.appId ?? '',
},
query: {
key: previewFileId ?? '',
node_id: apiContext.workflow?.nodeId,
},
},
}),
enabled: shouldDownloadPreviewFile && !!apiContext.workflow,
})
const downloadQuery = apiContext.workflow ? workflowDownloadQuery : agentDownloadQuery
const handleRemove = useCallback(() => {
onRemove(file.id)
}, [file.id, onRemove])
const handlePreviewOpenChange = useCallback((open: boolean) => {
if (open)
setSelectedFileId(file.id)
setIsPreviewOpen(open)
}, [file.id])
return (
<li className="group/file-row relative min-w-0">
<Dialog open={isPreviewOpen} onOpenChange={setIsPreviewOpen}>
<button
type="button"
data-selected={selected || undefined}
aria-current={selected ? 'true' : undefined}
className="group/file-tree-row relative flex h-6 w-full min-w-0 cursor-pointer items-center rounded-md pr-7 pl-2 text-left outline-hidden select-none hover:bg-state-base-hover focus-visible:bg-state-base-hover focus-visible:ring-2 focus-visible:ring-state-accent-solid data-[selected]:bg-state-base-active"
onClick={() => setIsPreviewOpen(true)}
<Dialog open={isPreviewOpen} onOpenChange={handlePreviewOpenChange}>
<DialogTrigger
render={(
<button
type="button"
data-selected={selected || undefined}
aria-current={selected ? 'true' : undefined}
className="group/file-tree-row relative flex h-6 w-full min-w-0 cursor-pointer items-center rounded-md pr-7 pl-2 text-left outline-hidden select-none hover:bg-state-base-hover focus-visible:bg-state-base-hover focus-visible:ring-2 focus-visible:ring-state-accent-solid focus-visible:ring-inset data-[selected]:bg-state-base-active"
/>
)}
>
{Array.from({ length: Math.max(depth - 1, 0) }, (_, index) => (
<FileTreeGuide key={index} />
@ -79,23 +179,28 @@ function AgentFileItem({
<div className="flex min-w-0 flex-[1_0_0] items-center py-0.5">
{children}
</div>
</button>
{isPreviewOpen && (
<AgentSkillDetailDialog
skillName={file.name}
detail={{
description: t('agentDetail.configure.files.tip'),
files,
filePreview: {
content: previewQuery.data?.content,
isError: previewQuery.isError,
isLoading: previewQuery.isPending,
},
selectedFileId: file.id,
sections: [],
}}
/>
)}
</DialogTrigger>
<AgentSkillDetailDialog
skillName={file.name}
detail={{
description: t('agentDetail.configure.files.tip'),
files,
filePreview: {
binary: previewQuery.data?.binary,
content: previewQuery.data?.text ?? undefined,
downloadUrl: downloadQuery.data?.url,
fileName: selectedPreviewFile.name,
isDownloadError: downloadQuery.isError,
isDownloadLoading: shouldDownloadPreviewFile && downloadQuery.isPending,
isError: previewQuery.isError,
isImage: isImagePreviewFile,
isLoading: previewQuery.isPending,
},
onSelectFile: selectedFile => setSelectedFileId(selectedFile.id),
selectedFileId: selectedFileId ?? file.id,
sections: [],
}}
/>
</Dialog>
{!readOnly && (
<button
@ -112,26 +217,107 @@ function AgentFileItem({
)
}
export function AgentFiles() {
export function AgentFiles({
agentId,
appId,
nodeId,
}: {
agentId: string
appId?: string
nodeId?: string
}) {
const { t } = useTranslation('agentV2')
const [files, setFiles] = useAtom(agentComposerFilesAtom)
const [draftFiles, setDraftFiles] = useAtom(agentComposerFilesAtom)
const filesTip = t('agentDetail.configure.files.tip')
const filesTreeId = 'agent-configure-files-tree'
const [isUploadOpen, setIsUploadOpen] = useState(false)
const promptAddCallbackRef = useRef<AgentOrchestrateAddActionOptions['onAdded']>(undefined)
const apiContext: AgentFileApiContext = useMemo(() => appId && nodeId
? {
agentId,
workflow: {
appId,
nodeId,
},
}
: { agentId }, [agentId, appId, nodeId])
const agentDriveFilesQuery = useQuery({
...consoleQuery.agent.byAgentId.drive.files.get.queryOptions({
input: {
params: {
agent_id: agentId,
},
query: {
prefix: FILES_DRIVE_PREFIX,
},
},
}),
enabled: !apiContext.workflow,
})
const workflowDriveFilesQuery = useQuery({
...consoleQuery.apps.byAppId.agent.drive.files.get.queryOptions({
input: {
params: {
app_id: appId ?? '',
},
query: {
node_id: nodeId,
prefix: FILES_DRIVE_PREFIX,
},
},
}),
enabled: !!apiContext.workflow,
})
const driveFilesQuery = apiContext.workflow ? workflowDriveFilesQuery : agentDriveFilesQuery
const { mutate: deleteAgentFile } = useMutation(consoleQuery.agent.byAgentId.files.delete.mutationOptions())
const { mutate: deleteWorkflowAgentFile } = useMutation(consoleQuery.apps.byAppId.agent.files.delete.mutationOptions())
const files = driveFilesQuery.isSuccess
? (driveFilesQuery.data.items ?? []).map(toAgentFileNodeFromDriveItem)
: draftFiles
const removeFile = useCallback((fileId: string) => {
setFiles(removeFileNode(files, fileId))
}, [files, setFiles])
const file = findAgentFileNode(files, fileId)
const driveKey = file?.driveKey
setDraftFiles(removeFileNode(draftFiles, fileId))
if (!driveKey)
return
const onSuccess = () => {
void driveFilesQuery.refetch()
}
if (apiContext.workflow) {
deleteWorkflowAgentFile({
params: {
app_id: apiContext.workflow.appId,
},
query: {
key: driveKey,
node_id: apiContext.workflow.nodeId,
},
}, { onSuccess })
return
}
deleteAgentFile({
params: {
agent_id: apiContext.agentId,
},
query: {
key: driveKey,
},
}, { onSuccess })
}, [apiContext, deleteAgentFile, deleteWorkflowAgentFile, draftFiles, driveFilesQuery, files, setDraftFiles])
const handleOpenUpload = useCallback((options?: AgentOrchestrateAddActionOptions) => {
promptAddCallbackRef.current = options?.onAdded
setIsUploadOpen(true)
}, [])
useRegisterAgentOrchestrateAddAction('files', handleOpenUpload)
const handleUploaded = useCallback((file: AgentFileNode) => {
setFiles([...files, file])
setDraftFiles([...draftFiles, file])
void driveFilesQuery.refetch()
promptAddCallbackRef.current?.(file)
promptAddCallbackRef.current = undefined
}, [files, setFiles])
}, [draftFiles, driveFilesQuery, setDraftFiles])
const handleUploadOpenChange = useCallback((open: boolean) => {
if (!open)
promptAddCallbackRef.current = undefined
@ -173,6 +359,7 @@ export function AgentFiles() {
depth={depth}
file={file}
files={files}
apiContext={apiContext}
selected={selected}
onRemove={removeFile}
>
@ -183,6 +370,7 @@ export function AgentFiles() {
)}
</ConfigureSection>
<AgentFileUploadDialog
apiContext={apiContext}
open={isUploadOpen}
onOpenChange={handleUploadOpenChange}
onUploaded={handleUploaded}

View File

@ -1,7 +1,7 @@
'use client'
import type { FileResponse } from '@dify/contracts/api/console/files/types.gen'
import type { FileTreeIconType } from '@langgenius/dify-ui/file-tree'
import type { AgentFileApiContext } from './api-context'
import type { AgentFileNode } from '@/features/agent-v2/agent-composer/form-state'
import { Button } from '@langgenius/dify-ui/button'
import { cn } from '@langgenius/dify-ui/cn'
@ -14,59 +14,23 @@ import { useTranslation } from 'react-i18next'
import ActionButton from '@/app/components/base/action-button'
import { consoleQuery } from '@/service/client'
import { formatFileSize } from '@/utils/format'
import { getFileIconType } from './file-icon'
const codeFileExtensions = new Set([
'css',
'go',
'html',
'js',
'jsx',
'py',
'rb',
'rs',
'scss',
'sh',
'ts',
'tsx',
'vue',
'yaml',
'yml',
])
const tableFileExtensions = new Set(['csv', 'xls', 'xlsx'])
const archiveFileExtensions = new Set(['7z', 'gz', 'rar', 'tar', 'zip'])
function getFileExtension(fileName: string) {
return fileName.split('.').pop()?.toLowerCase() ?? ''
type AgentDriveFileCommit = {
file: {
drive_key: string
file_id: string
mime_type?: string | null
name: string
}
}
function getFileIconType(fileName: string, mimeType?: string | null): FileTreeIconType {
const extension = getFileExtension(fileName)
if (mimeType?.startsWith('image/'))
return 'image'
if (mimeType === 'application/pdf' || extension === 'pdf')
return 'pdf'
if (extension === 'md' || extension === 'markdown' || extension === 'mdx')
return 'markdown'
if (extension === 'json')
return 'json'
if (tableFileExtensions.has(extension))
return 'table'
if (archiveFileExtensions.has(extension))
return 'archive'
if (codeFileExtensions.has(extension))
return 'code'
if (mimeType?.startsWith('text/'))
return 'text'
return 'file'
}
function toAgentFileNode(uploadedFile: FileResponse): AgentFileNode {
function toAgentFileNode(committedFile: AgentDriveFileCommit['file']): AgentFileNode {
return {
id: uploadedFile.id,
name: uploadedFile.name,
icon: getFileIconType(uploadedFile.name, uploadedFile.mime_type),
id: committedFile.file_id,
name: committedFile.name,
icon: getFileIconType(committedFile.name, committedFile.mime_type),
driveKey: committedFile.drive_key,
}
}
@ -168,10 +132,12 @@ function AgentFileUploader({
}
export function AgentFileUploadDialog({
apiContext,
open,
onOpenChange,
onUploaded,
}: {
apiContext: AgentFileApiContext
open: boolean
onOpenChange: (open: boolean) => void
onUploaded: (file: AgentFileNode) => void
@ -180,9 +146,43 @@ export function AgentFileUploadDialog({
const { t: tCommon } = useTranslation('common')
const [file, setFile] = useState<File>()
const uploadFileMutation = useMutation(consoleQuery.files.upload.post.mutationOptions())
const commitAgentFileMutation = useMutation(consoleQuery.agent.byAgentId.files.post.mutationOptions())
const commitWorkflowAgentFileMutation = useMutation(consoleQuery.apps.byAppId.agent.files.post.mutationOptions())
const isUploading = uploadFileMutation.isPending
|| commitAgentFileMutation.isPending
|| commitWorkflowAgentFileMutation.isPending
const commitUploadedFile = (uploadedFile: FileResponse, options: {
onSuccess: (committedFile: AgentDriveFileCommit) => void
onError: () => void
}) => {
const body = {
upload_file_id: uploadedFile.id,
}
if (apiContext.workflow) {
commitWorkflowAgentFileMutation.mutate({
params: {
app_id: apiContext.workflow.appId,
},
query: {
node_id: apiContext.workflow.nodeId,
},
body,
}, options)
return
}
commitAgentFileMutation.mutate({
params: {
agent_id: apiContext.agentId,
},
body,
}, options)
}
const handleUpload = () => {
if (!file || uploadFileMutation.isPending)
if (!file || isUploading)
return
uploadFileMutation.mutate({
@ -191,10 +191,17 @@ export function AgentFileUploadDialog({
},
}, {
onSuccess: (uploadedFile) => {
toast.success(t('agentDetail.configure.files.upload.success'))
onUploaded(toAgentFileNode(uploadedFile))
setFile(undefined)
onOpenChange(false)
commitUploadedFile(uploadedFile, {
onSuccess: (committedFile) => {
toast.success(t('agentDetail.configure.files.upload.success'))
onUploaded(toAgentFileNode(committedFile.file))
setFile(undefined)
onOpenChange(false)
},
onError: () => {
toast.error(t('agentDetail.configure.files.upload.failed'))
},
})
},
onError: () => {
toast.error(t('agentDetail.configure.files.upload.failed'))
@ -205,6 +212,8 @@ export function AgentFileUploadDialog({
const handleOpenChange = (nextOpen: boolean) => {
if (!nextOpen) {
uploadFileMutation.reset()
commitAgentFileMutation.reset()
commitWorkflowAgentFileMutation.reset()
setFile(undefined)
}
onOpenChange(nextOpen)
@ -225,14 +234,14 @@ export function AgentFileUploadDialog({
onChange={setFile}
/>
<div className="flex justify-end gap-2 pt-6">
<Button type="button" onClick={() => handleOpenChange(false)} disabled={uploadFileMutation.isPending}>
<Button type="button" onClick={() => handleOpenChange(false)} disabled={isUploading}>
{tCommon('operation.cancel')}
</Button>
<Button
type="button"
variant="primary"
disabled={!file}
loading={uploadFileMutation.isPending}
loading={isUploading}
onClick={handleUpload}
>
{t('agentDetail.configure.files.upload.action')}

View File

@ -1,6 +1,6 @@
'use client'
import type { AgentConfigSnapshotDetailResponse, AgentConfigSnapshotSummaryResponse, AgentPublishedReferenceResponse } from '@dify/contracts/api/console/agent/types.gen'
import type { AgentConfigSnapshotDetailResponse, AgentConfigSnapshotSummaryResponse } from '@dify/contracts/api/console/agent/types.gen'
import type { AgentConfigurePublishPayload } from './publish-bar'
import type { DefaultModel, Model } from '@/app/components/header/account-setting/model-provider-page/declarations'
import { cn } from '@langgenius/dify-ui/cn'
@ -20,6 +20,8 @@ import { AgentTools } from './tools'
type AgentOrchestratePanelProps = {
agentId: string
appId?: string
activeConfigIsPublished?: boolean
activeConfigSnapshot?: AgentConfigSnapshotSummaryResponse | null
agentSoulConfig?: AgentConfigSnapshotDetailResponse['config_snapshot']
agentName?: string | null
@ -27,8 +29,6 @@ type AgentOrchestratePanelProps = {
textGenerationModelList: Model[]
draftSavedAt?: number
isPublishing?: boolean
publishedReferenceCount?: number
publishedReferences?: AgentPublishedReferenceResponse[]
className?: string
readOnly?: boolean
showHeader?: boolean
@ -40,6 +40,8 @@ type AgentOrchestratePanelProps = {
export function AgentOrchestratePanel({
agentId,
appId,
activeConfigIsPublished,
activeConfigSnapshot,
agentSoulConfig,
agentName,
@ -47,8 +49,6 @@ export function AgentOrchestratePanel({
textGenerationModelList,
draftSavedAt,
isPublishing,
publishedReferenceCount,
publishedReferences,
className,
readOnly = false,
showHeader = true,
@ -87,7 +87,10 @@ export function AgentOrchestratePanel({
/>
<AgentPromptEditor />
<AgentSkills agentId={agentId} />
<AgentFiles />
<AgentFiles
agentId={agentId}
appId={appId}
/>
<AgentTools />
<AgentKnowledgeRetrieval />
<AgentAdvancedSettings />
@ -99,14 +102,13 @@ export function AgentOrchestratePanel({
{showPublishBar && (
<AgentConfigurePublishBar
agentId={agentId}
activeConfigIsPublished={activeConfigIsPublished}
activeConfigSnapshot={activeConfigSnapshot}
agentSoulConfig={agentSoulConfig}
agentName={agentName}
currentModel={currentModel}
draftSavedAt={draftSavedAt}
isPublishing={isPublishing}
publishedReferenceCount={publishedReferenceCount}
publishedReferences={publishedReferences}
onPublish={onPublish}
onOpenVersions={onOpenVersions}
/>

View File

@ -77,6 +77,32 @@ describe('AgentKnowledgeRetrieval', () => {
name: 'agentV2.agentDetail.configure.knowledgeRetrieval.remove:{"name":"agentV2.agentDetail.configure.knowledgeRetrieval.retrievalOne"}',
})).not.toBeInTheDocument()
})
it('should keep row actions focusable while visually hidden until hover or focus', () => {
renderKnowledgeRetrieval()
const editButton = screen.getByRole('button', {
name: 'agentV2.agentDetail.configure.knowledgeRetrieval.edit:{"name":"agentV2.agentDetail.configure.knowledgeRetrieval.retrievalOne"}',
})
const removeButton = screen.getByRole('button', {
name: 'agentV2.agentDetail.configure.knowledgeRetrieval.remove:{"name":"agentV2.agentDetail.configure.knowledgeRetrieval.retrievalOne"}',
})
const actionGroup = editButton.parentElement
expect(actionGroup).toHaveClass('flex')
expect(actionGroup).not.toHaveClass('hidden')
expect(actionGroup).toHaveClass(
'opacity-0',
'group-focus-within:opacity-100',
'group-hover:opacity-100',
)
expect(removeButton).toHaveClass(
'hover:bg-state-destructive-hover',
'hover:text-text-destructive',
'focus-visible:bg-state-destructive-hover',
'focus-visible:text-text-destructive',
)
})
})
describe('User Interactions', () => {

View File

@ -43,7 +43,7 @@ const queryModeOptions: KnowledgeRetrievalQueryMode[] = ['agent', 'custom']
const optionCardClassName = cn(
'flex h-8 flex-1 items-center justify-center rounded-lg border border-components-option-card-option-border bg-components-option-card-option-bg px-3 py-2 text-center system-sm-regular text-text-secondary transition-colors',
'hover:border-components-option-card-option-border-hover hover:bg-components-option-card-option-bg-hover',
'focus-visible:ring-2 focus-visible:ring-state-accent-solid focus-visible:outline-hidden',
'focus-visible:ring-1 focus-visible:ring-state-accent-solid focus-visible:outline-hidden',
'data-checked:border-[1.5px] data-checked:border-components-option-card-option-selected-border data-checked:bg-components-option-card-option-selected-bg data-checked:font-medium data-checked:text-text-primary data-checked:shadow-xs data-checked:shadow-shadow-shadow-3',
)

View File

@ -355,12 +355,12 @@ export function AgentPromptEditor() {
onKeyUpCapture={handleEditorKeyUp}
onPointerUpCapture={handleEditorPointerUp}
>
<div ref={editorRef} className="min-h-[72px] overflow-y-auto px-3 pt-0.5">
<div ref={editorRef} className="min-h-[104px] overflow-y-auto px-3 pt-0.5">
<PromptEditor
instanceId="agent-configure-prompt-editor"
compact
wrapperClassName="min-h-[72px]"
className="min-h-[72px] text-text-primary"
wrapperClassName="min-h-[104px]"
className="min-h-[104px] text-text-primary"
placeholder={promptPlaceholder}
placeholderClassName="top-0!"
editable={!readOnly}

View File

@ -1,17 +1,20 @@
'use client'
import type { AgentConfigSnapshotDetailResponse, AgentConfigSnapshotSummaryResponse, AgentPublishedReferenceResponse, AgentSoulConfig } from '@dify/contracts/api/console/agent/types.gen'
import type { AgentConfigSnapshotDetailResponse, AgentConfigSnapshotSummaryResponse, AgentSoulConfig } from '@dify/contracts/api/console/agent/types.gen'
import type { RegisterableHotkey } from '@tanstack/react-hotkeys'
import { Button } from '@langgenius/dify-ui/button'
import { cn } from '@langgenius/dify-ui/cn'
import { Kbd, KbdGroup } from '@langgenius/dify-ui/kbd'
import { StatusDot } from '@langgenius/dify-ui/status-dot'
import { formatForDisplay, useHotkey } from '@tanstack/react-hotkeys'
import { formatForDisplay } from '@tanstack/react-hotkeys'
import { useEffect, useRef, useState } from 'react'
import { useTranslation } from 'react-i18next'
import { useConfigPublishPayload, useHasAgentComposerUnpublishedChanges } from '@/features/agent-v2/agent-composer/store'
import { useFormatTimeFromNow } from '@/hooks/use-format-time-from-now'
import { AgentPublishImpactPopover } from './publish-impact-popover'
const PUBLISH_AGENT_HOTKEY = 'Mod+Shift+P'
const PUBLISH_AGENT_HOTKEY = 'Mod+Shift+P' satisfies RegisterableHotkey
const PUBLISH_IMPACT_BAR_HIDE_DELAY = 160
export type AgentConfigurePublishPayload = {
agent_id: string
@ -22,6 +25,7 @@ type AgentConfigurePublishState = 'draft' | 'publishing' | 'published' | 'unpubl
type AgentConfigurePublishBarProps = {
agentId: string
activeConfigIsPublished?: boolean
activeConfigSnapshot?: AgentConfigSnapshotSummaryResponse | null
agentSoulConfig?: AgentConfigSnapshotDetailResponse['config_snapshot']
agentName?: string | null
@ -31,17 +35,17 @@ type AgentConfigurePublishBarProps = {
}
draftSavedAt?: number
isPublishing?: boolean
publishedReferenceCount?: number
publishedReferences?: AgentPublishedReferenceResponse[]
onPublish?: (payload: AgentConfigurePublishPayload) => void | Promise<void>
onOpenVersions: () => void
}
function getPublishState({
activeConfigIsPublished,
activeConfigSnapshot,
isDirty,
isPublishing,
}: {
activeConfigIsPublished?: boolean
activeConfigSnapshot?: AgentConfigSnapshotSummaryResponse | null
isDirty: boolean
isPublishing: boolean
@ -52,7 +56,7 @@ function getPublishState({
if (!activeConfigSnapshot)
return 'draft'
if (isDirty)
if (!activeConfigIsPublished || isDirty)
return 'unpublished'
return 'published'
@ -70,19 +74,20 @@ function PublishShortcut() {
export function AgentConfigurePublishBar({
agentId,
activeConfigIsPublished,
activeConfigSnapshot,
agentSoulConfig,
agentName,
currentModel,
draftSavedAt,
isPublishing = false,
publishedReferenceCount = 0,
publishedReferences = [],
onPublish,
onOpenVersions,
}: AgentConfigurePublishBarProps) {
const { t } = useTranslation('agentV2')
const { formatTimeFromNow } = useFormatTimeFromNow()
const [shouldHidePublishBar, setShouldHidePublishBar] = useState(false)
const hidePublishBarTimerRef = useRef<ReturnType<typeof setTimeout> | undefined>(undefined)
const hasUnpublishedChanges = useHasAgentComposerUnpublishedChanges()
const publishPayload = useConfigPublishPayload({
agentId,
@ -90,12 +95,27 @@ export function AgentConfigurePublishBar({
currentModel,
})
const publishState = getPublishState({
activeConfigIsPublished,
activeConfigSnapshot,
isDirty: hasUnpublishedChanges,
isPublishing,
})
const canPublish = publishState === 'draft' || publishState === 'unpublished'
const handleImpactPopoverOpenChange = (open: boolean) => {
if (hidePublishBarTimerRef.current)
clearTimeout(hidePublishBarTimerRef.current)
if (!open) {
setShouldHidePublishBar(false)
return
}
hidePublishBarTimerRef.current = setTimeout(() => {
setShouldHidePublishBar(true)
}, PUBLISH_IMPACT_BAR_HIDE_DELAY)
}
const handlePublish = () => {
if (!canPublish)
return
@ -103,13 +123,12 @@ export function AgentConfigurePublishBar({
void onPublish?.(publishPayload)
}
useHotkey(PUBLISH_AGENT_HOTKEY, (event) => {
event.preventDefault()
handlePublish()
}, {
enabled: canPublish,
ignoreInputs: false,
})
useEffect(() => {
return () => {
if (hidePublishBarTimerRef.current)
clearTimeout(hidePublishBarTimerRef.current)
}
}, [])
const publishedMeta = activeConfigSnapshot?.created_at
? t('agentDetail.configure.publishBar.publishedAt', {
@ -166,7 +185,13 @@ export function AgentConfigurePublishBar({
return (
<div className="flex h-16 shrink-0 items-center justify-center px-4 pt-2 pb-3">
<div className="flex max-w-full min-w-0 items-center gap-2 rounded-xl border-[0.5px] border-components-panel-border bg-components-panel-bg-blur p-2 shadow-lg shadow-shadow-shadow-5 backdrop-blur-[5px]">
<div
className={cn(
'flex max-w-full min-w-0 items-center gap-2 rounded-xl border-[0.5px] border-components-panel-border bg-components-panel-bg-blur p-2 shadow-lg shadow-shadow-shadow-5 backdrop-blur-[5px]',
shouldHidePublishBar && 'pointer-events-none opacity-0',
)}
aria-hidden={shouldHidePublishBar}
>
<div className="flex min-w-0 items-center gap-1 px-2 system-xs-regular text-text-tertiary">
<span className="flex size-4 shrink-0 items-center justify-center">
<StatusDot size="small" status={currentStateMeta.dotStatus} />
@ -187,10 +212,12 @@ export function AgentConfigurePublishBar({
</button>
<AgentPublishImpactPopover
actionLabel={currentStateMeta.actionLabel}
actionShortcut={currentStateMeta.showShortcut ? <PublishShortcut /> : null}
hotkey={PUBLISH_AGENT_HOTKEY}
agentId={agentId}
agentName={agentName}
disabled={!canPublish}
publishedReferenceCount={publishedReferenceCount}
publishedReferences={publishedReferences}
onOpenChange={handleImpactPopoverOpenChange}
onPublish={handlePublish}
trigger={(
<Button

View File

@ -1,76 +1,122 @@
'use client'
import type { AgentPublishedReferenceResponse } from '@dify/contracts/api/console/agent/types.gen'
import type { MouseEvent, ReactElement } from 'react'
import type { AgentIconType, AgentReferencingWorkflowResponse } from '@dify/contracts/api/console/agent/types.gen'
import type { RegisterableHotkey } from '@tanstack/react-hotkeys'
import type { MouseEvent, ReactElement, ReactNode } from 'react'
import { Button } from '@langgenius/dify-ui/button'
import { cn } from '@langgenius/dify-ui/cn'
import { Popover, PopoverContent, PopoverTrigger } from '@langgenius/dify-ui/popover'
import { useHotkey } from '@tanstack/react-hotkeys'
import { useQuery } from '@tanstack/react-query'
import { cloneElement, useState } from 'react'
import { useTranslation } from 'react-i18next'
import AppIcon from '@/app/components/base/app-icon'
import Link from '@/next/link'
import { consoleQuery } from '@/service/client'
type AgentPublishImpactPopoverProps = {
actionLabel: string
actionShortcut?: ReactNode
hotkey: RegisterableHotkey
agentId: string
agentName?: string | null
disabled?: boolean
publishedReferenceCount?: number
publishedReferences?: AgentPublishedReferenceResponse[]
trigger: ReactElement<{
onClick?: (event: MouseEvent<HTMLElement>) => void
}>
onOpenChange?: (open: boolean) => void
onPublish: () => void
}
const workflowReferenceAvatarClassNames = [
'bg-components-icon-bg-green-soft text-components-icon-bg-green-solid',
'bg-components-icon-bg-orange-dark-soft text-components-icon-bg-orange-dark-solid',
'bg-components-icon-bg-pink-soft text-components-icon-bg-pink-solid',
'bg-components-icon-bg-blue-soft text-components-icon-bg-blue-solid',
] as const
const getWorkflowReferenceHref = (reference: AgentPublishedReferenceResponse) => `/app/${reference.app_id}/workflow`
const getWorkflowReferenceInitial = (name: string) => {
return name.trim().charAt(0).toUpperCase() || '?'
}
const getWorkflowReferenceHref = (reference: AgentReferencingWorkflowResponse) => `/app/${reference.app_id}/workflow`
export function AgentPublishImpactPopover({
actionLabel,
actionShortcut,
hotkey,
agentId,
agentName,
disabled = false,
publishedReferenceCount = 0,
publishedReferences = [],
trigger,
onOpenChange,
onPublish,
}: AgentPublishImpactPopoverProps) {
const { t } = useTranslation('agentV2')
const [open, setOpen] = useState(false)
const hasPublishedReferences = publishedReferenceCount > 0 && publishedReferences.length > 0
if (!hasPublishedReferences || disabled)
return trigger
const triggerWithImpact = cloneElement(trigger, {
onClick: (event: MouseEvent<HTMLElement>) => {
event.preventDefault()
setOpen(true)
},
const [isCheckingReferences, setIsCheckingReferences] = useState(false)
const workflowReferencesQuery = useQuery({
...consoleQuery.agent.byAgentId.referencingWorkflows.get.queryOptions({
input: {
params: {
agent_id: agentId,
},
},
}),
enabled: false,
})
const publishedReferences = workflowReferencesQuery.data?.data ?? []
const updateOpen = (nextOpen: boolean) => {
setOpen(nextOpen)
onOpenChange?.(nextOpen)
}
const handlePublish = () => {
setOpen(false)
updateOpen(false)
onPublish()
}
const handlePublishRequest = async () => {
if (disabled || isCheckingReferences)
return
if (open) {
handlePublish()
return
}
setIsCheckingReferences(true)
try {
const result = await workflowReferencesQuery.refetch()
const references = result.data?.data ?? []
if (references.length > 0) {
updateOpen(true)
return
}
onPublish()
}
finally {
setIsCheckingReferences(false)
}
}
useHotkey(hotkey, (event) => {
event.preventDefault()
void handlePublishRequest()
}, {
enabled: !disabled,
ignoreInputs: false,
})
if (disabled)
return trigger
const triggerWithImpact = cloneElement(trigger, {
onClick: async (event: MouseEvent<HTMLElement>) => {
event.preventDefault()
await handlePublishRequest()
},
})
return (
<Popover open={open} onOpenChange={setOpen}>
<Popover open={open} onOpenChange={updateOpen}>
<PopoverTrigger
render={triggerWithImpact}
/>
<PopoverContent
placement="top-end"
sideOffset={-40}
popupClassName="w-96 overflow-hidden rounded-xl border-[0.5px] border-components-panel-border bg-components-panel-bg-blur p-0 shadow-lg shadow-shadow-shadow-5 backdrop-blur-[5px]"
sideOffset={-32}
popupClassName="w-96 overflow-hidden rounded-xl border-[0.5px] border-components-panel-border bg-components-panel-bg-blur p-0 shadow-lg shadow-shadow-shadow-5 backdrop-blur-[5px] transition-[transform,scale,opacity] duration-150 ease-out data-starting-style:translate-y-5 data-ending-style:translate-y-5 motion-reduce:transition-none motion-reduce:data-starting-style:translate-y-0 motion-reduce:data-ending-style:translate-y-0"
>
<div className="flex flex-col gap-0">
<div className="flex flex-col gap-0.5 px-3 pt-3.5 pb-1">
@ -84,7 +130,7 @@ export function AgentPublishImpactPopover({
{t('agentDetail.configure.publishImpact.descriptionPrefix')}
{' '}
<span className="system-xs-medium">
{t('agentDetail.configure.publishImpact.workflowCount', { count: publishedReferenceCount })}
{t('agentDetail.configure.publishImpact.workflowCount', { count: publishedReferences.length })}
</span>
{t('agentDetail.configure.publishImpact.descriptionSuffix')}
</p>
@ -95,8 +141,8 @@ export function AgentPublishImpactPopover({
{t('agentDetail.configure.publishImpact.affectedWorkflows')}
</div>
<div className="flex max-h-44 flex-col gap-px overflow-y-auto rounded-xl border border-components-panel-border p-1">
{publishedReferences.map((reference, index) => (
<ReferenceLink key={`${reference.app_id}-${reference.workflow_id}`} reference={reference} index={index} />
{publishedReferences.map(reference => (
<ReferenceLink key={`${reference.app_id}-${reference.workflow_id}`} reference={reference} />
))}
</div>
</div>
@ -106,17 +152,18 @@ export function AgentPublishImpactPopover({
type="button"
variant="secondary"
className="h-8 min-w-18 rounded-lg px-3"
onClick={() => setOpen(false)}
onClick={() => updateOpen(false)}
>
{t('agentDetail.configure.publishImpact.cancel')}
</Button>
<Button
type="button"
variant="primary"
className="h-8 min-w-18 rounded-lg px-3"
className="h-8 min-w-18 gap-1 rounded-lg px-3"
onClick={handlePublish}
>
{actionLabel}
<span className="shrink-0">{actionLabel}</span>
{actionShortcut}
</Button>
</div>
</div>
@ -127,11 +174,12 @@ export function AgentPublishImpactPopover({
function ReferenceLink({
reference,
index,
}: {
reference: AgentPublishedReferenceResponse
index: number
reference: AgentReferencingWorkflowResponse
}) {
const imageUrl = (reference.app_icon_type === 'image' || reference.app_icon_type === 'link') ? reference.app_icon : undefined
const iconType = (imageUrl ? 'image' : reference.app_icon_type) as AgentIconType | null | undefined
return (
<Link
href={getWorkflowReferenceHref(reference)}
@ -139,14 +187,14 @@ function ReferenceLink({
rel="noopener noreferrer"
className="flex min-w-0 items-center gap-2 rounded-lg py-1 pr-2.5 pl-2 system-sm-regular text-text-secondary hover:bg-state-base-hover focus-visible:ring-2 focus-visible:ring-state-accent-solid focus-visible:outline-hidden"
>
<span
aria-hidden
className={cn(
'flex size-5 shrink-0 items-center justify-center rounded-md border-[0.5px] border-divider-regular system-xs-medium',
workflowReferenceAvatarClassNames[index % workflowReferenceAvatarClassNames.length],
)}
>
{getWorkflowReferenceInitial(reference.app_name)}
<span aria-hidden className="shrink-0">
<AppIcon
size="tiny"
iconType={iconType}
icon={reference.app_icon ?? undefined}
background={reference.app_icon_background}
imageUrl={imageUrl}
/>
</span>
<span className="min-w-0 flex-1 truncate">{reference.app_name}</span>
<span aria-hidden className="i-ri-external-link-line size-3 shrink-0 text-text-tertiary" />

View File

@ -11,6 +11,8 @@ import { AgentSkills } from '../index'
const mocks = vi.hoisted(() => ({
driveFilesQueryOptions: vi.fn(),
driveFileDownloadQueryOptions: vi.fn(),
driveFilePreviewQueryOptions: vi.fn(),
uploadSkillMutationOptions: vi.fn(),
}))
@ -30,6 +32,16 @@ vi.mock('@/service/client', () => ({
get: {
queryOptions: mocks.driveFilesQueryOptions,
},
download: {
get: {
queryOptions: mocks.driveFileDownloadQueryOptions,
},
},
preview: {
get: {
queryOptions: mocks.driveFilePreviewQueryOptions,
},
},
},
},
skills: {
@ -51,7 +63,7 @@ const agentSkillsDraft = {
id: 'tender-analyzer',
name: 'Tender Analyzer',
description: 'Extracts tender requirements and scoring criteria.',
files: ['SKILL.md', 'schema.json'],
files: ['__MACOSX/._hatch-pet', 'SKILL.md', 'schema.json'],
path: 'tender-analyzer',
skillMdKey: 'tender-analyzer/SKILL.md',
},
@ -129,6 +141,18 @@ describe('AgentSkills', () => {
],
}),
}))
mocks.driveFilePreviewQueryOptions.mockImplementation(({ input }) => ({
queryKey: ['agent-drive-file-preview', input],
queryFn: async () => ({
text: `Preview content for ${input.query.key}`,
}),
}))
mocks.driveFileDownloadQueryOptions.mockImplementation(({ input }) => ({
queryKey: ['agent-drive-file-download', input],
queryFn: async () => ({
url: `https://example.com/${input.query.key}`,
}),
}))
mocks.uploadSkillMutationOptions.mockReturnValue({
mutationFn: vi.fn(),
mutationKey: ['upload-skill'],
@ -152,14 +176,60 @@ describe('AgentSkills', () => {
agent_id: 'agent-1',
},
query: {
prefix: 'tender-analyzer',
prefix: 'tender-analyzer/',
},
},
})
expect(within(dialog).getByText('Tender Analyzer')).toBeInTheDocument()
expect(within(dialog).getByText('Extracts tender requirements and scoring criteria.')).toBeInTheDocument()
expect(within(dialog).queryByText('__MACOSX/._hatch-pet')).not.toBeInTheDocument()
expect(await within(dialog).findByText('scripts/extract.py')).toBeInTheDocument()
expect(within(dialog).getByText('SKILL.md')).toBeInTheDocument()
expect(await within(dialog).findByText('Preview content for tender-analyzer/SKILL.md')).toBeInTheDocument()
expect(mocks.driveFilePreviewQueryOptions).toHaveBeenCalledWith({
input: {
params: {
agent_id: 'agent-1',
},
query: {
key: 'tender-analyzer/SKILL.md',
},
},
})
expect(mocks.driveFilePreviewQueryOptions).not.toHaveBeenCalledWith({
input: {
params: {
agent_id: 'agent-1',
},
query: {
key: 'tender-analyzer/__MACOSX/._hatch-pet',
},
},
})
})
it('should preview the selected skill file from the detail file tree', async () => {
renderAgentSkills()
fireEvent.click(screen.getByRole('button', {
name: 'Tender Analyzer',
}))
const dialog = screen.getByRole('dialog')
const scriptFile = await within(dialog).findByRole('button', { name: 'scripts/extract.py' })
fireEvent.click(scriptFile)
expect(await within(dialog).findByText('Preview content for tender-analyzer/scripts/extract.py')).toBeInTheDocument()
expect(mocks.driveFilePreviewQueryOptions).toHaveBeenCalledWith({
input: {
params: {
agent_id: 'agent-1',
},
query: {
key: 'tender-analyzer/scripts/extract.py',
},
},
})
})
// The hover/focus remove action updates the composer draft without opening preview.
@ -235,7 +305,7 @@ describe('AgentSkills', () => {
agent_id: 'agent-1',
},
query: {
prefix: 'invoice-helper',
prefix: 'invoice-helper/',
},
},
})

View File

@ -7,6 +7,7 @@ import {
DialogDescription,
DialogTitle,
} from '@langgenius/dify-ui/dialog'
import { FileTreeFile } from '@langgenius/dify-ui/file-tree'
import { ScrollArea } from '@langgenius/dify-ui/scroll-area'
import { useTranslation } from 'react-i18next'
import Loading from '@/app/components/base/loading'
@ -27,10 +28,17 @@ export type AgentSkillDetail = {
fileCount?: number
files: AgentSkillFileNode[]
filePreview?: {
binary?: boolean
content?: string
downloadUrl?: string
fileName?: string
isDownloadError?: boolean
isDownloadLoading?: boolean
isError?: boolean
isImage?: boolean
isLoading?: boolean
}
onSelectFile?: (file: AgentSkillFileNode) => void
selectedFileId?: string
sections: AgentSkillDetailSection[]
}
@ -38,10 +46,12 @@ export type AgentSkillDetail = {
function AgentSkillFileList({
files,
fileCount,
onSelectFile,
selectedFileId,
}: {
files: AgentSkillFileNode[]
fileCount: number
onSelectFile?: (file: AgentSkillFileNode) => void
selectedFileId?: string
}) {
const { t } = useTranslation('agentV2')
@ -52,6 +62,13 @@ function AgentSkillFileList({
selectedFileId={selectedFileId}
labelledBy="agent-skill-detail-files-heading"
className="h-[258px] rounded-xl border-[0.5px] border-components-panel-border bg-components-panel-on-panel-item-bg p-1 shadow-xs shadow-shadow-shadow-3"
renderFile={onSelectFile
? ({ file, selected, children }) => (
<FileTreeFile selected={selected} onClick={() => onSelectFile(file)}>
{children}
</FileTreeFile>
)
: undefined}
header={(
<>
<h3 id="agent-skill-detail-files-heading" className="sr-only">
@ -96,17 +113,30 @@ function AgentSkillDetailSectionBlock({
}
function AgentFilePreviewContent({
binary,
content,
downloadUrl,
fileName,
isDownloadError,
isDownloadLoading,
isError,
isImage,
isLoading,
}: {
binary?: boolean
content?: string
downloadUrl?: string
fileName?: string
isDownloadError?: boolean
isDownloadLoading?: boolean
isError?: boolean
isImage?: boolean
isLoading?: boolean
}) {
const { t } = useTranslation('agentV2')
const { t: tCommon } = useTranslation('common')
if (isLoading) {
if (isLoading || isDownloadLoading) {
return (
<div className="flex min-h-40 items-center justify-center">
<Loading type="area" />
@ -114,7 +144,7 @@ function AgentFilePreviewContent({
)
}
if (isError) {
if (isError || isDownloadError) {
return (
<p className="system-sm-regular text-text-tertiary">
{t('agentDetail.configure.files.preview.failed')}
@ -122,6 +152,45 @@ function AgentFilePreviewContent({
)
}
if (isImage && downloadUrl) {
return (
<div className="flex min-h-40 items-start justify-center">
<img
src={downloadUrl}
alt={fileName ?? ''}
className="max-h-[560px] max-w-full rounded-lg object-contain"
/>
</div>
)
}
if (binary) {
if (downloadUrl) {
return (
<div className="flex min-w-0 flex-wrap items-center gap-2">
<span className="system-sm-regular text-text-tertiary">
{t('agentDetail.configure.files.preview.unsupported')}
</span>
<a
href={downloadUrl}
target="_blank"
rel="noreferrer"
className="inline-flex min-w-0 items-center gap-1 rounded-md px-2 py-1 system-sm-medium text-text-accent outline-hidden hover:bg-state-base-hover focus-visible:ring-2 focus-visible:ring-state-accent-solid"
>
<span aria-hidden className="i-ri-download-2-line size-4 shrink-0" />
<span className="shrink-0">{tCommon('operation.download')}</span>
</a>
</div>
)
}
return (
<p className="system-sm-regular text-text-tertiary">
{t('agentDetail.configure.files.preview.empty')}
</p>
)
}
if (!content) {
return (
<p className="system-sm-regular text-text-tertiary">
@ -171,8 +240,14 @@ export function AgentSkillDetailDialog({
>
{detail.filePreview && (
<AgentFilePreviewContent
binary={detail.filePreview.binary}
content={detail.filePreview.content}
downloadUrl={detail.filePreview.downloadUrl}
fileName={detail.filePreview.fileName}
isDownloadError={detail.filePreview.isDownloadError}
isDownloadLoading={detail.filePreview.isDownloadLoading}
isError={detail.filePreview.isError}
isImage={detail.filePreview.isImage}
isLoading={detail.filePreview.isLoading}
/>
)}
@ -181,7 +256,12 @@ export function AgentSkillDetailDialog({
))}
</ScrollArea>
<div className="flex w-56 max-w-56 min-w-0 shrink-0 items-start justify-center p-4 pl-2">
<AgentSkillFileList files={detail.files} fileCount={fileCount} selectedFileId={detail.selectedFileId} />
<AgentSkillFileList
files={detail.files}
fileCount={fileCount}
selectedFileId={detail.selectedFileId}
onSelectFile={detail.onSelectFile}
/>
</div>
</div>
</DialogContent>

View File

@ -1,7 +1,7 @@
'use client'
import type { AgentDriveItemResponse } from '@dify/contracts/api/console/agent/types.gen'
import type { AgentSkill } from '@/features/agent-v2/agent-composer/form-state'
import type { AgentFileNode, AgentSkill } from '@/features/agent-v2/agent-composer/form-state'
import {
Dialog,
} from '@langgenius/dify-ui/dialog'
@ -9,6 +9,7 @@ import { useQuery } from '@tanstack/react-query'
import { useCallback, useState } from 'react'
import { useTranslation } from 'react-i18next'
import { consoleQuery } from '@/service/client'
import { getDriveFileIconType } from '../files/file-icon'
import { useAgentOrchestrateReadOnly } from '../read-only-context'
import { AgentSkillDetailDialog } from './detail-dialog'
@ -22,11 +23,37 @@ const getSkillFileName = (key: string, skillDrivePath: string) => key.startsWith
: key
const toSkillFileNode = (item: AgentDriveItemResponse, skillDrivePath: string) => ({
icon: item.file_kind === 'folder' ? 'folder' as const : 'file' as const,
icon: getDriveFileIconType({
fileKind: item.file_kind,
fileName: getSkillFileName(item.key, skillDrivePath),
mimeType: item.mime_type,
}),
id: item.key,
name: getSkillFileName(item.key, skillDrivePath),
})
const getSkillMdFileId = (files: AgentFileNode[]): string | undefined => {
for (const file of files) {
if (file.icon !== 'folder' && file.name === 'SKILL.md')
return file.id
const childFileId = file.children ? getSkillMdFileId(file.children) : undefined
if (childFileId)
return childFileId
}
}
const getFirstSkillFileId = (files: AgentFileNode[]): string | undefined => {
for (const file of files) {
if (file.icon !== 'folder')
return file.id
const childFileId = file.children ? getFirstSkillFileId(file.children) : undefined
if (childFileId)
return childFileId
}
}
export function AgentSkillItem({
agentId,
skill,
@ -39,10 +66,14 @@ export function AgentSkillItem({
const { t } = useTranslation('agentV2')
const readOnly = useAgentOrchestrateReadOnly()
const [isPreviewOpen, setIsPreviewOpen] = useState(false)
const [selectedFileId, setSelectedFileId] = useState<string>()
const handleRemove = useCallback(() => {
onRemove(skill.id)
}, [onRemove, skill.id])
const skillFiles = skill.files ?? []
const handleOpenPreview = useCallback(() => {
setSelectedFileId(undefined)
setIsPreviewOpen(true)
}, [])
const skillDrivePath = getSkillDrivePath(skill)
const driveFilesQuery = useQuery({
...consoleQuery.agent.byAgentId.drive.files.get.queryOptions({
@ -51,7 +82,7 @@ export function AgentSkillItem({
agent_id: agentId,
},
query: {
prefix: skillDrivePath,
prefix: `${skillDrivePath}/`,
},
},
}),
@ -59,11 +90,38 @@ export function AgentSkillItem({
})
const detailFiles = driveFilesQuery.isSuccess
? (driveFilesQuery.data.items ?? []).map(item => toSkillFileNode(item, skillDrivePath))
: skillFiles.map(file => ({
icon: 'file' as const,
id: file,
name: file,
}))
: []
const previewFileId = selectedFileId
?? skill.skillMdKey
?? (driveFilesQuery.isSuccess ? getSkillMdFileId(detailFiles) ?? getFirstSkillFileId(detailFiles) : undefined)
const previewQuery = useQuery({
...consoleQuery.agent.byAgentId.drive.files.preview.get.queryOptions({
input: {
params: {
agent_id: agentId,
},
query: {
key: previewFileId ?? '',
},
},
}),
enabled: isPreviewOpen && !!previewFileId,
})
const selectedFile = detailFiles.find(file => file.id === previewFileId)
const isImagePreviewFile = selectedFile?.icon === 'image'
const downloadQuery = useQuery({
...consoleQuery.agent.byAgentId.drive.files.download.get.queryOptions({
input: {
params: {
agent_id: agentId,
},
query: {
key: previewFileId ?? '',
},
},
}),
enabled: isPreviewOpen && !!previewFileId && (isImagePreviewFile || !!previewQuery.data?.binary),
})
return (
<Dialog open={isPreviewOpen} onOpenChange={setIsPreviewOpen}>
@ -71,7 +129,7 @@ export function AgentSkillItem({
<button
type="button"
className="flex h-full min-w-0 flex-1 cursor-pointer items-center gap-1 rounded-md text-left outline-hidden focus-visible:ring-2 focus-visible:ring-state-accent-solid"
onClick={() => setIsPreviewOpen(true)}
onClick={handleOpenPreview}
>
<span aria-hidden className="i-custom-public-agent-building-blocks size-4 shrink-0" />
<span className="min-w-0 flex-1 truncate system-sm-medium text-text-secondary">
@ -103,6 +161,19 @@ export function AgentSkillItem({
detail={{
description: skill.description ?? t('agentDetail.configure.skills.tip'),
files: detailFiles,
filePreview: {
binary: previewQuery.data?.binary,
content: previewQuery.data?.text ?? undefined,
downloadUrl: downloadQuery.data?.url,
fileName: selectedFile?.name,
isDownloadError: downloadQuery.isError,
isDownloadLoading: (isImagePreviewFile || !!previewQuery.data?.binary) && downloadQuery.isPending,
isError: previewQuery.isError,
isImage: isImagePreviewFile,
isLoading: previewQuery.isPending,
},
onSelectFile: file => setSelectedFileId(file.id),
selectedFileId: previewFileId,
sections: [],
}}
/>

View File

@ -113,6 +113,7 @@ function AgentConfigurePageLoadedContent({
>
<AgentOrchestratePanel
agentId={agentId}
activeConfigIsPublished={agentQuery.data?.active_config_is_published}
activeConfigSnapshot={activeConfigSnapshot}
agentSoulConfig={agentSoulConfig}
agentName={agentQuery.data?.name}

View File

@ -88,6 +88,18 @@ export function useAgentConfigureSync({
consoleQuery.agent.byAgentId.composer.get.queryKey({ input: { params: { agent_id: agentId } } }),
composerState,
)
queryClient.setQueryData(
consoleQuery.agent.byAgentId.get.queryKey({ input: { params: { agent_id: agentId } } }),
(agentDetail) => {
if (!agentDetail)
return agentDetail
return {
...agentDetail,
active_config_is_published: true,
}
},
)
void queryClient.invalidateQueries({
queryKey: consoleQuery.agent.byAgentId.versions.get.key(),
})

View File

@ -0,0 +1,303 @@
import type { AgentLogConversationItemResponse } from '@dify/contracts/api/console/agent/types.gen'
import type { ReactNode, TdHTMLAttributes, ThHTMLAttributes } from 'react'
import { Button } from '@langgenius/dify-ui/button'
import { cn } from '@langgenius/dify-ui/cn'
import {
ScrollAreaContent,
ScrollAreaRoot,
ScrollAreaScrollbar,
ScrollAreaThumb,
ScrollAreaViewport,
} from '@langgenius/dify-ui/scroll-area'
import { useTranslation } from 'react-i18next'
import useTimestamp from '@/hooks/use-timestamp'
import { LogSourceCell } from './source-cell'
export function AgentLogsTable({
logs,
isPending,
isError,
isSuccess,
onRetry,
}: {
logs: AgentLogConversationItemResponse[]
isPending: boolean
isError: boolean
isSuccess: boolean
onRetry: () => void
}) {
const { t } = useTranslation('agentV2')
const tableHeaderLabels = {
unread: t('agentDetail.logs.table.unread'),
title: t('agentDetail.logs.table.title'),
source: t('agentDetail.logs.table.source'),
endUser: t('agentDetail.logs.table.endUser'),
messageCount: t('agentDetail.logs.table.messageCount'),
userRate: t('agentDetail.logs.table.userRate'),
operationRate: t('agentDetail.logs.table.operationRate'),
updatedTime: t('agentDetail.logs.table.updatedTime'),
createdTime: t('agentDetail.logs.table.createdTime'),
}
return (
<div className="flex h-full min-w-0 flex-col overflow-hidden">
<div className="shrink-0">
<table aria-hidden="true" className="w-full table-fixed border-collapse">
<LogsTableColGroup />
<LogsTableHeader labels={tableHeaderLabels} />
</table>
</div>
<ScrollAreaRoot className="relative min-h-0 flex-1 overflow-hidden">
<ScrollAreaViewport
aria-label={t('agentDetail.logs.title')}
role="region"
tabIndex={-1}
className="overscroll-contain"
>
<ScrollAreaContent>
<table className="w-full table-fixed border-collapse">
<LogsTableColGroup />
<LogsTableHeader labels={tableHeaderLabels} rowClassName="sr-only" />
<AgentLogsTableBody
logs={logs}
isPending={isPending}
isError={isError}
isSuccess={isSuccess}
onRetry={onRetry}
/>
</table>
</ScrollAreaContent>
</ScrollAreaViewport>
<ScrollAreaScrollbar className="data-[orientation=vertical]:translate-x-1">
<ScrollAreaThumb />
</ScrollAreaScrollbar>
</ScrollAreaRoot>
</div>
)
}
function AgentLogsTableBody({
logs,
isPending,
isError,
isSuccess,
onRetry,
}: {
logs: AgentLogConversationItemResponse[]
isPending: boolean
isError: boolean
isSuccess: boolean
onRetry: () => void
}) {
const { t } = useTranslation('agentV2')
const { t: tCommon } = useTranslation('common')
const { formatTime } = useTimestamp()
const notAvailable = t('agentDetail.logs.notAvailable')
const formatLogTime = (value?: number | null) =>
value == null ? notAvailable : formatTime(value, t('roster.dateTimeFormat'))
return (
<tbody className="system-sm-regular text-text-secondary">
{isPending && (
<LogsSkeletonRows />
)}
{isError && (
<LogsStateRow>
<div className="flex items-center justify-center gap-2">
<span>{t('agentDetail.logs.loadFailed')}</span>
<Button variant="secondary" size="small" onClick={onRetry}>
{tCommon('operation.retry')}
</Button>
</div>
</LogsStateRow>
)}
{isSuccess && logs.length === 0 && (
<LogsStateRow>
{t('agentDetail.logs.empty')}
</LogsStateRow>
)}
{isSuccess && logs.map(log => (
<tr
key={log.id}
className="h-10 border-b border-divider-subtle hover:bg-background-default-hover"
>
<td className="px-0">
<span className={cn(
'mx-auto block size-1.5 rounded-full',
log.unread ? 'bg-util-colors-blue-blue-500' : 'bg-transparent',
)}
/>
</td>
<TableCell className="system-sm-medium text-text-secondary">
{log.title || notAvailable}
</TableCell>
<td className="px-3">
<LogSourceCell source={log.source} />
</td>
<TableCell translate="no">
{log.end_user_id || notAvailable}
</TableCell>
<TableCell className="tabular-nums">
{log.message_count}
</TableCell>
<TableCell className="text-text-quaternary">
{formatRate(log.user_rate, notAvailable)}
</TableCell>
<TableCell className="text-text-quaternary">
{formatRate(log.operation_rate, notAvailable)}
</TableCell>
<TableCell>
{formatLogTime(log.updated_at)}
</TableCell>
<TableCell>
{formatLogTime(log.created_at)}
</TableCell>
</tr>
))}
</tbody>
)
}
function formatRate(value: number | null | undefined, fallback: string) {
return value == null ? fallback : `${Math.round(value * 100)}%`
}
type LogsTableHeaderLabels = {
unread: string
title: string
source: string
endUser: string
messageCount: string
userRate: string
operationRate: string
updatedTime: string
createdTime: string
}
function LogsTableHeader({
labels,
rowClassName,
}: {
labels: LogsTableHeaderLabels
rowClassName?: string
}) {
return (
<thead>
<tr className={cn('h-7 bg-background-section-burn text-left system-xs-medium-uppercase text-text-tertiary', rowClassName)}>
<th scope="col" className="rounded-l-lg px-0">
<span className="sr-only">{labels.unread}</span>
</th>
<TableHead>{labels.title}</TableHead>
<TableHead>{labels.source}</TableHead>
<TableHead>{labels.endUser}</TableHead>
<TableHead>{labels.messageCount}</TableHead>
<TableHead>{labels.userRate}</TableHead>
<TableHead>{labels.operationRate}</TableHead>
<TableHead>{labels.updatedTime}</TableHead>
<TableHead className="rounded-r-lg">{labels.createdTime}</TableHead>
</tr>
</thead>
)
}
function LogsTableColGroup() {
return (
<colgroup>
<col className="w-5" />
<col className="w-[28%]" />
<col className="w-[16%]" />
<col className="w-[15%]" />
<col className="w-[8%]" />
<col className="w-[7%]" />
<col className="w-[7%]" />
<col className="w-[10%]" />
<col className="w-[9%]" />
</colgroup>
)
}
function LogsStateRow({
children,
}: {
children: ReactNode
}) {
return (
<tr className="h-20 border-b border-divider-subtle">
<td colSpan={9} className="px-3 text-center text-text-tertiary">
{children}
</td>
</tr>
)
}
function LogsSkeletonRows() {
return (
<>
{Array.from({ length: 10 }, (_, index) => (
<tr key={index} className="h-10 border-b border-divider-subtle">
<td className="px-0">
<span className="mx-auto block size-1.5 rounded-full bg-text-quaternary opacity-20" />
</td>
<td className="px-3">
<div className="h-3 w-3/4 rounded-sm bg-text-quaternary opacity-20" />
</td>
<td className="px-3">
<div className="flex items-center gap-2">
<div className="size-5 rounded-full bg-text-quaternary opacity-20" />
<div className="h-3 w-24 rounded-sm bg-text-quaternary opacity-20" />
</div>
</td>
<td className="px-3">
<div className="h-3 w-28 rounded-sm bg-text-quaternary opacity-20" />
</td>
<td className="px-3">
<div className="h-3 w-8 rounded-sm bg-text-quaternary opacity-20" />
</td>
<td className="px-3">
<div className="h-3 w-8 rounded-sm bg-text-quaternary opacity-20" />
</td>
<td className="px-3">
<div className="h-3 w-8 rounded-sm bg-text-quaternary opacity-20" />
</td>
<td className="px-3">
<div className="h-3 w-24 rounded-sm bg-text-quaternary opacity-20" />
</td>
<td className="px-3">
<div className="h-3 w-24 rounded-sm bg-text-quaternary opacity-20" />
</td>
</tr>
))}
</>
)
}
function TableHead({
className,
...props
}: ThHTMLAttributes<HTMLTableCellElement>) {
return (
<th
scope="col"
className={cn('px-3 text-left whitespace-nowrap', className)}
{...props}
/>
)
}
function TableCell({
children,
className,
...props
}: TdHTMLAttributes<HTMLTableCellElement>) {
return (
<td
className={cn('min-w-0 px-3 whitespace-nowrap', className)}
{...props}
>
<div className="truncate">
{children}
</div>
</td>
)
}

View File

@ -0,0 +1,28 @@
import type { AgentLogConversationItemResponse } from '@dify/contracts/api/console/agent/types.gen'
import { useTranslation } from 'react-i18next'
import { LogSourceIcon } from './source-icon'
export function LogSourceCell({
source,
}: {
source?: AgentLogConversationItemResponse['source']
}) {
const { t } = useTranslation('agentV2')
if (!source) {
return (
<div className="truncate text-text-quaternary">
{t('agentDetail.logs.notAvailable')}
</div>
)
}
return (
<div className="flex min-w-0 items-center gap-2">
<LogSourceIcon source={source} />
<div className="min-w-0 flex-1 truncate">
{source.app_name}
</div>
</div>
)
}

View File

@ -0,0 +1,33 @@
import type { AgentIconType, AgentLogSourceResponse } from '@dify/contracts/api/console/agent/types.gen'
import AppIcon from '@/app/components/base/app-icon'
const getLogSourceImageUrl = (source?: AgentLogSourceResponse | null) =>
(source?.app_icon_type === 'image' || source?.app_icon_type === 'link')
? source.app_icon
: undefined
const getLogSourceIconType = (source?: AgentLogSourceResponse | null) => {
const imageUrl = getLogSourceImageUrl(source)
return (imageUrl ? 'image' : source?.app_icon_type) as AgentIconType | null | undefined
}
export function LogSourceIcon({
source,
}: {
source?: AgentLogSourceResponse | null
}) {
if (!source)
return <span aria-hidden className="i-ri-apps-2-line size-5 shrink-0 text-text-quaternary" />
return (
<AppIcon
size="xs"
rounded
iconType={getLogSourceIconType(source)}
icon={source.app_icon ?? undefined}
background={source.app_icon_background}
imageUrl={getLogSourceImageUrl(source)}
className="size-5 text-sm"
/>
)
}

View File

@ -0,0 +1,184 @@
import type { AgentLogSourceGroupResponse, AgentLogSourceResponse } from '@dify/contracts/api/console/agent/types.gen'
import type { TFunction } from 'i18next'
import type { ReactNode } from 'react'
import { Button } from '@langgenius/dify-ui/button'
import { cn } from '@langgenius/dify-ui/cn'
import {
Combobox,
ComboboxClear,
ComboboxCollection,
ComboboxContent,
ComboboxEmpty,
ComboboxGroup,
ComboboxGroupLabel,
ComboboxInput,
ComboboxInputGroup,
ComboboxItem,
ComboboxItemText,
ComboboxList,
ComboboxTrigger,
ComboboxValue,
} from '@langgenius/dify-ui/combobox'
import { useState } from 'react'
import { useTranslation } from 'react-i18next'
import { LogSourceIcon } from './source-icon'
export type SourceFilterValue = AgentLogSourceResponse['id'][]
const getSourceGroupLabel = (
group: AgentLogSourceGroupResponse,
t: TFunction<'agentV2'>,
) => {
if (group.type === 'webapp')
return t('agentDetail.logs.filters.source.webapp')
if (group.type === 'workflow')
return t('agentDetail.logs.filters.source.workflow')
return group.label
}
const getSourceLabel = (source: AgentLogSourceResponse) => source.app_name
export function AgentLogSourcePicker({
value,
groups,
isLoading,
isError,
onRetry,
onChange,
}: {
value: SourceFilterValue
groups: AgentLogSourceGroupResponse[]
isLoading: boolean
isError: boolean
onRetry: () => void
onChange: (value: SourceFilterValue) => void
}) {
const { t } = useTranslation('agentV2')
const { t: tCommon } = useTranslation('common')
const [inputValue, setInputValue] = useState('')
const sources = groups.flatMap(group => group.sources ?? [])
const selectedSources = sources.filter(source => value.includes(source.id))
return (
<Combobox<AgentLogSourceResponse, true>
multiple
items={groups}
value={selectedSources}
itemToStringLabel={getSourceLabel}
onValueChange={(nextSources) => {
setInputValue('')
onChange(nextSources.map(source => source.id))
}}
inputValue={inputValue}
onInputValueChange={setInputValue}
>
<ComboboxTrigger
aria-label={t('agentDetail.logs.filters.source.label')}
className="mt-0 w-fit max-w-full min-w-22"
>
<ComboboxValue placeholder={t('agentDetail.logs.filters.source.all')}>
{(selectedValue: AgentLogSourceResponse[]) => {
if (selectedValue.length === 0)
return t('agentDetail.logs.filters.source.all')
if (selectedValue.length === 1)
return selectedValue[0]!.app_name
return tCommon('dynamicSelect.selected', { count: selectedValue.length })
}}
</ComboboxValue>
</ComboboxTrigger>
<ComboboxContent popupClassName="w-80 p-0">
<div className="p-2 pb-1">
<ComboboxInputGroup className="h-8 min-h-8 px-2">
<span aria-hidden className="mr-0.5 i-ri-search-line size-4 shrink-0 text-components-input-text-placeholder" />
<ComboboxInput
aria-label={t('agentDetail.logs.filters.source.searchLabel')}
placeholder={t('agentDetail.logs.filters.source.searchPlaceholder')}
className="block h-4.5 grow px-1 py-0 system-sm-regular text-components-input-text-filled"
/>
<ComboboxClear className="mr-0" />
</ComboboxInputGroup>
</div>
{isLoading && (
<SourcePickerStatus>
{t('agentDetail.logs.filters.source.loading')}
</SourcePickerStatus>
)}
{isError && (
<SourcePickerStatus className="flex items-center justify-center gap-2">
<span>{t('agentDetail.logs.filters.source.loadFailed')}</span>
<Button variant="secondary" size="small" onClick={onRetry}>
{t('operation.retry', { ns: 'common' })}
</Button>
</SourcePickerStatus>
)}
{!isLoading && !isError && (
<>
<ComboboxList className="max-h-69 p-2 pt-1">
{groups.map(group => (
<ComboboxGroup key={group.type} items={group.sources ?? []}>
<ComboboxGroupLabel className="px-1 pt-2 pb-1">
{getSourceGroupLabel(group, t)}
</ComboboxGroupLabel>
<ComboboxCollection>
{(source: AgentLogSourceResponse) => (
<ComboboxItem
key={source.id}
value={source}
className="min-h-7 grid-cols-[1fr] gap-0 px-1 py-1"
>
<ComboboxItemText className="flex min-w-0 items-center gap-2 px-0 system-sm-regular">
<SourceCheckbox checked={value.includes(source.id)} />
<LogSourceIcon source={source} />
<span className="min-w-0 flex-1 truncate">
{source.app_name}
</span>
</ComboboxItemText>
</ComboboxItem>
)}
</ComboboxCollection>
</ComboboxGroup>
))}
</ComboboxList>
<ComboboxEmpty className="px-3 py-3 text-center system-xs-regular">
{t('agentDetail.logs.filters.source.empty')}
</ComboboxEmpty>
</>
)}
</ComboboxContent>
</Combobox>
)
}
function SourcePickerStatus({
children,
className,
}: {
children: ReactNode
className?: string
}) {
return (
<div className={cn('px-3 py-3 text-center system-xs-regular text-text-tertiary', className)}>
{children}
</div>
)
}
function SourceCheckbox({
checked,
}: {
checked: boolean
}) {
return (
<span
aria-hidden
className={cn(
'flex size-4 shrink-0 items-center justify-center rounded-sm border shadow-xs shadow-shadow-shadow-3',
checked
? 'border-transparent bg-components-checkbox-bg text-components-checkbox-icon'
: 'border-components-checkbox-border bg-components-checkbox-bg-unchecked',
)}
>
{checked && <span className="i-ri-check-line size-3" />}
</span>
)
}

View File

@ -1,376 +0,0 @@
import type { I18nKeysWithPrefix } from '@/types/i18n'
export type PeriodKey = 'last7days' | 'last30days' | 'allTime'
export type SourceKey = 'all' | 'webapp' | 'workflow'
export type FilterOption<T extends string> = {
value: T
labelKey: I18nKeysWithPrefix<'agentV2', 'agentDetail.logs.'>
}
type AgentLogRow = {
id: string
title: string
endUser: string
messageCount: number
userRate: string
operationRate: string
updatedTime: string
createdTime: string
source: Exclude<SourceKey, 'all'>
unread?: boolean
}
export const periodOptions: Array<FilterOption<PeriodKey>> = [
{ value: 'last7days', labelKey: 'agentDetail.logs.filters.period.last7days' },
{ value: 'last30days', labelKey: 'agentDetail.logs.filters.period.last30days' },
{ value: 'allTime', labelKey: 'agentDetail.logs.filters.period.allTime' },
]
export const sourceOptions: Array<FilterOption<SourceKey>> = [
{ value: 'all', labelKey: 'agentDetail.logs.filters.source.all' },
{ value: 'webapp', labelKey: 'agentDetail.logs.filters.source.webapp' },
{ value: 'workflow', labelKey: 'agentDetail.logs.filters.source.workflow' },
]
const LOG_ROW_COUNT = 5400
const LOG_INTERVAL_MINUTES = 2
const baseLogTime = new Date(2023, 2, 21, 10, 25)
const periodDays: Partial<Record<PeriodKey, number>> = {
last7days: 7,
last30days: 30,
}
const logTemplates: AgentLogRow[] = [
{
id: 'log_001',
title: 'Asking about Dify agent orchestration best practices',
endUser: '94924171-e6b0-4076-8f54-71d4370af8ef',
messageCount: 2,
userRate: 'N/A',
operationRate: 'N/A',
updatedTime: '2023-03-21 10:25',
createdTime: '2023-03-21 10:25',
source: 'webapp',
unread: true,
},
{
id: 'log_002',
title: 'Alice, our user, talks about prompt orchestration techniques',
endUser: 'N/A',
messageCount: 3,
userRate: 'N/A',
operationRate: 'N/A',
updatedTime: '2023-03-21 10:25',
createdTime: '2023-03-21 10:25',
source: 'workflow',
unread: true,
},
{
id: 'log_003',
title: 'How to self-host a Dify chatbot for an internal team',
endUser: '94924171-e6b0-4076-8f54-71d4370af8ef',
messageCount: 5,
userRate: 'N/A',
operationRate: 'N/A',
updatedTime: '2023-03-21 10:25',
createdTime: '2023-03-21 10:25',
source: 'webapp',
unread: true,
},
{
id: 'log_004',
title: 'Requesting information about dataset retrieval settings',
endUser: '94924171-e6b0-4076-8f54-71d4370af8ef',
messageCount: 1,
userRate: 'N/A',
operationRate: 'N/A',
updatedTime: '2023-03-21 10:25',
createdTime: '2023-03-21 10:25',
source: 'workflow',
unread: true,
},
{
id: 'log_005',
title: 'Exploring options for connecting external knowledge bases',
endUser: 'N/A',
messageCount: 3,
userRate: 'N/A',
operationRate: 'N/A',
updatedTime: '2023-03-21 10:25',
createdTime: '2023-03-21 10:25',
source: 'webapp',
unread: true,
},
{
id: 'log_006',
title: 'What types of plugin tools can be used in workflows?',
endUser: '94924171-e6b0-4076-8f54-71d4370af8ef',
messageCount: 2,
userRate: 'N/A',
operationRate: 'N/A',
updatedTime: '2023-03-21 10:25',
createdTime: '2023-03-21 10:25',
source: 'workflow',
unread: true,
},
{
id: 'log_007',
title: 'Querying about Dify cloud deployment requirements',
endUser: '94924171-e6b0-4076-8f54-71d4370af8ef',
messageCount: 2,
userRate: 'N/A',
operationRate: 'N/A',
updatedTime: '2023-03-21 10:25',
createdTime: '2023-03-21 10:25',
source: 'webapp',
unread: true,
},
{
id: 'log_008',
title: 'Seeking assistance with YAML file setup',
endUser: '94924171-e6b0-4076-8f54-71d4370af8ef',
messageCount: 2,
userRate: 'N/A',
operationRate: 'N/A',
updatedTime: '2023-03-21 10:25',
createdTime: '2023-03-21 10:25',
source: 'workflow',
unread: true,
},
{
id: 'log_009',
title: 'Inquiring about compatibility with external APIs',
endUser: '94924171-e6b0-4076-8f54-71d4370af8ef',
messageCount: 5,
userRate: 'N/A',
operationRate: 'N/A',
updatedTime: '2023-03-21 10:25',
createdTime: '2023-03-21 10:25',
source: 'webapp',
unread: true,
},
{
id: 'log_010',
title: 'Can Dify integrate with my existing CRM?',
endUser: '94924171-e6b0-4076-8f54-71d4370af8ef',
messageCount: 2,
userRate: 'N/A',
operationRate: 'N/A',
updatedTime: '2023-03-21 10:25',
createdTime: '2023-03-21 10:25',
source: 'workflow',
unread: true,
},
{
id: 'log_011',
title: 'Exploring options for customizing chatbot responses',
endUser: '94924171-e6b0-4076-8f54-71d4370af8ef',
messageCount: 2,
userRate: 'N/A',
operationRate: 'N/A',
updatedTime: '2023-03-21 10:25',
createdTime: '2023-03-21 10:25',
source: 'webapp',
unread: true,
},
{
id: 'log_012',
title: 'Understanding data management and security in Dify',
endUser: '94924171-e6b0-4076-8f54-71d4370af8ef',
messageCount: 2,
userRate: 'N/A',
operationRate: 'N/A',
updatedTime: '2023-03-21 10:25',
createdTime: '2023-03-21 10:25',
source: 'workflow',
unread: true,
},
{
id: 'log_013',
title: 'Learning about available resources for getting started with Dify',
endUser: '94924171-e6b0-4076-8f54-71d4370af8ef',
messageCount: 2,
userRate: 'N/A',
operationRate: 'N/A',
updatedTime: '2023-03-21 10:25',
createdTime: '2023-03-21 10:25',
source: 'webapp',
unread: true,
},
{
id: 'log_014',
title: 'What are the best practices for optimizing prompts?',
endUser: '94924171-e6b0-4076-8f54-71d4370af8ef',
messageCount: 2,
userRate: 'N/A',
operationRate: 'N/A',
updatedTime: '2023-03-21 10:25',
createdTime: '2023-03-21 10:25',
source: 'workflow',
unread: true,
},
{
id: 'log_015',
title: 'How do I monitor the performance of my AI applications?',
endUser: '94924171-e6b0-4076-8f54-71d4370af8ef',
messageCount: 2,
userRate: 'N/A',
operationRate: 'N/A',
updatedTime: '2023-03-21 10:25',
createdTime: '2023-03-21 10:25',
source: 'webapp',
unread: true,
},
{
id: 'log_016',
title: 'Is there a free trial available for Dify?',
endUser: '94924171-e6b0-4076-8f54-71d4370af8ef',
messageCount: 2,
userRate: 'N/A',
operationRate: 'N/A',
updatedTime: '2023-03-21 10:25',
createdTime: '2023-03-21 10:25',
source: 'workflow',
unread: true,
},
{
id: 'log_017',
title: 'How can I improve user satisfaction metrics with Dify?',
endUser: '94924171-e6b0-4076-8f54-71d4370af8ef',
messageCount: 2,
userRate: 'N/A',
operationRate: 'N/A',
updatedTime: '2023-03-21 10:25',
createdTime: '2023-03-21 10:25',
source: 'webapp',
unread: true,
},
{
id: 'log_018',
title: 'Are there any upcoming features or improvements in Dify?',
endUser: '94924171-e6b0-4076-8f54-71d4370af8ef',
messageCount: 2,
userRate: 'N/A',
operationRate: 'N/A',
updatedTime: '2023-03-21 10:25',
createdTime: '2023-03-21 10:25',
source: 'workflow',
unread: true,
},
{
id: 'log_019',
title: 'What are the recommended steps to deploy a Dify chatbot?',
endUser: '94924171-e6b0-4076-8f54-71d4370af8ef',
messageCount: 2,
userRate: 'N/A',
operationRate: 'N/A',
updatedTime: '2023-03-21 10:25',
createdTime: '2023-03-21 10:25',
source: 'webapp',
unread: true,
},
]
const formatDateTime = (date: Date) => {
const year = date.getFullYear()
const month = String(date.getMonth() + 1).padStart(2, '0')
const day = String(date.getDate()).padStart(2, '0')
const hour = String(date.getHours()).padStart(2, '0')
const minute = String(date.getMinutes()).padStart(2, '0')
return `${year}-${month}-${day} ${hour}:${minute}`
}
const formatLogTime = (index: number, offset: number) => {
const logTime = new Date(baseLogTime)
logTime.setMinutes(logTime.getMinutes() - index * LOG_INTERVAL_MINUTES - offset)
return formatDateTime(logTime)
}
const getPeriodThreshold = (period: PeriodKey) => {
const days = periodDays[period]
if (!days)
return undefined
const threshold = new Date(baseLogTime)
threshold.setDate(threshold.getDate() - days)
return formatDateTime(threshold)
}
const logRows: AgentLogRow[] = Array.from({ length: LOG_ROW_COUNT }, (_, index) => {
const template = logTemplates[index % logTemplates.length]!
return {
...template,
id: `${template.id}_${index + 1}`,
messageCount: template.messageCount + (index % 4),
updatedTime: formatLogTime(index, 0),
createdTime: formatLogTime(index, 3),
}
})
export function getOption<T extends string>(options: Array<FilterOption<T>>, value: T) {
return options.find(option => option.value === value) ?? options[0]!
}
export function getSortParts(sortBy: string) {
return {
sortOrder: sortBy.startsWith('-') ? '-' : '',
sortValue: sortBy.replace('-', '') || 'created_at',
}
}
export function getAgentLogRowsView({
period,
source,
keyword,
sortBy,
page,
limit,
}: {
period: PeriodKey
source: SourceKey
keyword: string
sortBy: string
page: number
limit: number
}) {
const normalizedKeyword = keyword.trim().toLowerCase()
const periodThreshold = getPeriodThreshold(period)
const filteredRows = logRows.filter((log) => {
const matchesPeriod = !periodThreshold || log.createdTime >= periodThreshold
const matchesSource = source === 'all' || log.source === source
const matchesKeyword = !normalizedKeyword || [
log.title,
log.endUser,
String(log.messageCount),
log.updatedTime,
log.createdTime,
].some(value => value.toLowerCase().includes(normalizedKeyword))
return matchesPeriod && matchesSource && matchesKeyword
})
const { sortOrder, sortValue } = getSortParts(sortBy)
const sortField = sortValue === 'updated_at' ? 'updatedTime' : 'createdTime'
const sortDirection = sortOrder ? -1 : 1
const sortedRows = [...filteredRows].sort((a, b) => {
const timeSort = a[sortField].localeCompare(b[sortField]) * sortDirection
if (timeSort !== 0)
return timeSort
return a.title.localeCompare(b.title) * sortDirection
})
const totalPages = Math.max(Math.ceil(sortedRows.length / limit), 1)
const currentPage = Math.min(page, totalPages)
return {
currentPage,
totalPages,
rows: sortedRows.slice((currentPage - 1) * limit, currentPage * limit),
}
}

View File

@ -1,73 +1,87 @@
'use client'
import type { TdHTMLAttributes, ThHTMLAttributes } from 'react'
import type {
PeriodKey,
SourceKey,
} from './mock-data'
import { cn } from '@langgenius/dify-ui/cn'
import type { SourceFilterValue } from './components/source-picker'
import { Pagination } from '@langgenius/dify-ui/pagination'
import {
ScrollAreaContent,
ScrollAreaRoot,
ScrollAreaScrollbar,
ScrollAreaThumb,
ScrollAreaViewport,
} from '@langgenius/dify-ui/scroll-area'
import { Select, SelectContent, SelectItem, SelectItemIndicator, SelectItemText, SelectTrigger } from '@langgenius/dify-ui/select'
import { useQuery } from '@tanstack/react-query'
import dayjs from 'dayjs'
import { useState } from 'react'
import { useTranslation } from 'react-i18next'
import Chip from '@/app/components/base/chip'
import { SearchInput } from '@/app/components/base/search-input'
import Sort from '@/app/components/base/sort'
import { useDocLink } from '@/context/i18n'
import {
getAgentLogRowsView,
getOption,
getSortParts,
periodOptions,
sourceOptions,
} from './mock-data'
import { consoleQuery } from '@/service/client'
import { AgentLogsTable } from './components/logs-table'
import { AgentLogSourcePicker } from './components/source-picker'
export function AgentLogsPage() {
type PeriodKey = 'last7days' | 'last30days' | 'allTime'
type AgentLogsPageProps = {
agentId: string
}
const queryDateFormat = 'YYYY-MM-DD HH:mm'
const periodOptions: Array<{
value: PeriodKey
labelKey: 'agentDetail.logs.filters.period.last7days' | 'agentDetail.logs.filters.period.last30days' | 'agentDetail.logs.filters.period.allTime'
}> = [
{ value: 'last7days', labelKey: 'agentDetail.logs.filters.period.last7days' },
{ value: 'last30days', labelKey: 'agentDetail.logs.filters.period.last30days' },
{ value: 'allTime', labelKey: 'agentDetail.logs.filters.period.allTime' },
]
const getPeriodQuery = (period: PeriodKey) => {
if (period === 'allTime')
return {}
const days = period === 'last7days' ? 7 : 30
return {
start: dayjs().subtract(days, 'day').format(queryDateFormat),
end: dayjs().add(1, 'minute').format(queryDateFormat),
}
}
export function AgentLogsPage({
agentId,
}: AgentLogsPageProps) {
const { t } = useTranslation('agentV2')
const { t: tCommon } = useTranslation('common')
const docLink = useDocLink()
const [period, setPeriod] = useState<PeriodKey>('last7days')
const [source, setSource] = useState<SourceKey>('all')
const [source, setSource] = useState<SourceFilterValue>([])
const [keyword, setKeyword] = useState('')
const [sortBy, setSortBy] = useState('-created_at')
const [page, setPage] = useState(2)
const [page, setPage] = useState(1)
const [limit, setLimit] = useState(25)
const selectedSource = getOption(sourceOptions, source)
const { sortOrder, sortValue } = getSortParts(sortBy)
const tableHeaderLabels = {
unread: t('agentDetail.logs.table.unread'),
title: t('agentDetail.logs.table.title'),
endUser: t('agentDetail.logs.table.endUser'),
messageCount: t('agentDetail.logs.table.messageCount'),
userRate: t('agentDetail.logs.table.userRate'),
operationRate: t('agentDetail.logs.table.operationRate'),
updatedTime: t('agentDetail.logs.table.updatedTime'),
createdTime: t('agentDetail.logs.table.createdTime'),
}
const periodItems = periodOptions.map(option => ({
value: option.value,
name: t(option.labelKey),
}))
const {
currentPage,
totalPages,
rows,
} = getAgentLogRowsView({
period,
source,
keyword,
sortBy,
page,
limit,
})
const logSourcesQuery = useQuery(consoleQuery.agent.byAgentId.logSources.get.queryOptions({
input: {
params: {
agent_id: agentId,
},
},
}))
const logsQuery = useQuery(consoleQuery.agent.byAgentId.logs.get.queryOptions({
input: {
params: {
agent_id: agentId,
},
query: {
...getPeriodQuery(period),
page,
limit,
keyword: keyword.trim() || undefined,
// TODO: Send multiple source ids after the backend contract supports multi-source filtering.
source: source.length === 1 ? source[0] : undefined,
},
},
}))
const logs = logsQuery.data?.data ?? []
const totalPages = Math.max(Math.ceil((logsQuery.data?.total ?? 0) / limit), 1)
const currentPage = logsQuery.data?.page ?? page
return (
<section
@ -110,30 +124,19 @@ export function AgentLogsPage() {
}}
/>
<Select
<AgentLogSourcePicker
value={source}
onValueChange={(nextValue) => {
if (nextValue) {
setPage(1)
setSource(nextValue as SourceKey)
}
groups={logSourcesQuery.data?.groups ?? []}
isLoading={logSourcesQuery.isPending}
isError={logSourcesQuery.isError}
onRetry={() => {
void logSourcesQuery.refetch()
}}
>
<SelectTrigger
aria-label={t('agentDetail.logs.filters.source.label')}
className="mt-0 w-fit max-w-full min-w-22"
>
{t(selectedSource.labelKey)}
</SelectTrigger>
<SelectContent popupClassName="w-80">
{sourceOptions.map(option => (
<SelectItem key={option.value} value={option.value}>
<SelectItemText>{t(option.labelKey)}</SelectItemText>
<SelectItemIndicator />
</SelectItem>
))}
</SelectContent>
</Select>
onChange={(nextSource) => {
setPage(1)
setSource(nextSource)
}}
/>
<SearchInput
aria-label={t('agentDetail.logs.filters.search.label')}
@ -148,93 +151,27 @@ export function AgentLogsPage() {
</div>
<Sort
order={sortOrder}
value={sortValue}
order="desc"
value="updated_at"
items={[
{ value: 'created_at', name: t('agentDetail.logs.filters.sort.lastCreatedTime') },
{ value: 'updated_at', name: t('agentDetail.logs.filters.sort.lastUpdatedTime') },
]}
onSelect={(value) => {
setPage(1)
setSortBy(value)
}}
onSelect={() => {}}
/>
</div>
</header>
<div className="min-h-0 flex-1 px-6 pt-2 pb-3">
<div className="flex h-full min-w-0 flex-col overflow-x-auto">
<div className="min-w-[1212px] shrink-0">
<table aria-hidden="true" className="w-full table-fixed border-collapse">
<LogsTableColGroup />
<LogsTableHeader labels={tableHeaderLabels} />
</table>
</div>
<ScrollAreaRoot className="relative min-h-0 min-w-[1212px] flex-1 overflow-hidden">
<ScrollAreaViewport
aria-label={t('agentDetail.logs.title')}
role="region"
tabIndex={-1}
className="overscroll-contain"
>
<ScrollAreaContent>
<table className="w-full table-fixed border-collapse">
<LogsTableColGroup />
<LogsTableHeader labels={tableHeaderLabels} rowClassName="sr-only" />
<tbody className="system-sm-regular text-text-secondary">
{rows.length > 0
? rows.map(log => (
<tr
key={log.id}
className="h-10 border-b border-divider-subtle hover:bg-background-default-hover"
>
<td className="px-0">
<span className={cn(
'mx-auto block size-1.5 rounded-full',
log.unread ? 'bg-util-colors-blue-blue-500' : 'bg-transparent',
)}
/>
</td>
<TableCell className="system-sm-medium text-text-secondary">
{log.title}
</TableCell>
<TableCell translate="no">
{log.endUser}
</TableCell>
<TableCell>
{log.messageCount}
</TableCell>
<TableCell className="text-text-quaternary">
{log.userRate}
</TableCell>
<TableCell className="text-text-quaternary">
{log.operationRate}
</TableCell>
<TableCell>
{log.updatedTime}
</TableCell>
<TableCell>
{log.createdTime}
</TableCell>
</tr>
))
: (
<tr className="h-20 border-b border-divider-subtle">
<td colSpan={8} className="px-3 text-center text-text-tertiary">
{t('agentDetail.logs.empty')}
</td>
</tr>
)}
</tbody>
</table>
</ScrollAreaContent>
</ScrollAreaViewport>
<ScrollAreaScrollbar className="data-[orientation=vertical]:translate-x-1">
<ScrollAreaThumb />
</ScrollAreaScrollbar>
</ScrollAreaRoot>
</div>
<AgentLogsTable
logs={logs}
isPending={logsQuery.isPending}
isError={logsQuery.isError}
isSuccess={logsQuery.isSuccess}
onRetry={() => {
void logsQuery.refetch()
}}
/>
</div>
<Pagination
@ -262,84 +199,3 @@ export function AgentLogsPage() {
</section>
)
}
type LogsTableHeaderLabels = {
unread: string
title: string
endUser: string
messageCount: string
userRate: string
operationRate: string
updatedTime: string
createdTime: string
}
function LogsTableHeader({
labels,
rowClassName,
}: {
labels: LogsTableHeaderLabels
rowClassName?: string
}) {
return (
<thead>
<tr className={cn('h-7 bg-background-section-burn text-left system-xs-medium-uppercase text-text-tertiary', rowClassName)}>
<th scope="col" className="rounded-l-lg px-0">
<span className="sr-only">{labels.unread}</span>
</th>
<TableHead>{labels.title}</TableHead>
<TableHead>{labels.endUser}</TableHead>
<TableHead>{labels.messageCount}</TableHead>
<TableHead>{labels.userRate}</TableHead>
<TableHead>{labels.operationRate}</TableHead>
<TableHead>{labels.updatedTime}</TableHead>
<TableHead className="rounded-r-lg">{labels.createdTime}</TableHead>
</tr>
</thead>
)
}
function LogsTableColGroup() {
return (
<colgroup>
<col className="w-5" />
<col className="w-[42%]" />
<col className="w-[14%]" />
<col className="w-24" />
<col className="w-20" />
<col className="w-18" />
<col className="w-34" />
<col className="w-34" />
</colgroup>
)
}
function TableHead({
className,
...props
}: ThHTMLAttributes<HTMLTableCellElement>) {
return (
<th
scope="col"
className={cn('px-3 text-left whitespace-nowrap', className)}
{...props}
/>
)
}
function TableCell({
children,
className,
...props
}: TdHTMLAttributes<HTMLTableCellElement>) {
return (
<td
className={cn('min-w-0 px-3 whitespace-nowrap', className)}
{...props}
>
<div className="truncate">
{children}
</div>
</td>
)
}

View File

@ -19,7 +19,7 @@ export function AgentDetailPage({
return <AgentMonitoringPage agentId={agentId} />
if (section === 'logs')
return <AgentLogsPage />
return <AgentLogsPage agentId={agentId} />
if (section === 'access')
return <AgentAccessPage agentId={agentId} />

View File

@ -1,16 +1,47 @@
import type { ComponentProps } from 'react'
import type { AgentRosterListItem } from '../agent-roster-list'
import { toast } from '@langgenius/dify-ui/toast'
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
import { render, screen } from '@testing-library/react'
import { render, screen, waitFor } from '@testing-library/react'
import userEvent from '@testing-library/user-event'
import { AgentRosterList } from '../agent-roster-list'
const { duplicateAgentMutationFn } = vi.hoisted(() => ({
duplicateAgentMutationFn: vi.fn(),
}))
vi.mock('@/hooks/use-timestamp', () => ({
default: () => ({
formatTime: () => '06/12/2026 12:00:00 PM',
}),
}))
vi.mock('@/service/client', () => ({
consoleQuery: {
agent: {
byAgentId: {
copy: {
post: {
mutationOptions: () => ({
mutationFn: duplicateAgentMutationFn,
}),
},
},
delete: {
mutationOptions: () => ({
mutationFn: vi.fn(),
}),
},
put: {
mutationOptions: () => ({
mutationFn: vi.fn(),
}),
},
},
},
},
}))
const createAgent = (overrides: Partial<AgentRosterListItem> = {}): AgentRosterListItem => ({
active_config_is_published: false,
description: 'Find and summarize market materials.',
@ -51,6 +82,16 @@ const renderList = (
describe('AgentRosterList', () => {
beforeEach(() => {
vi.clearAllMocks()
vi.spyOn(toast, 'error').mockReturnValue('toast-id')
vi.spyOn(toast, 'success').mockReturnValue('toast-id')
duplicateAgentMutationFn.mockResolvedValue(createAgent({
id: 'agent-copy',
name: 'Research Agent copy',
}))
})
afterEach(() => {
vi.restoreAllMocks()
})
it('renders role under the card title instead of the agent mode', () => {
@ -68,6 +109,28 @@ describe('AgentRosterList', () => {
expect(screen.getByText('agentV2.roster.usageStatus.draft')).toHaveClass('system-2xs-medium-uppercase')
})
it('draws the primary link focus ring above the draft corner label without z-index', () => {
renderList([createAgent()])
const configureLink = screen.getByRole('link', { name: 'Research Agent' })
const draftLabel = screen.getByText('agentV2.roster.usageStatus.draft')
const draftCornerLabel = draftLabel.closest('.absolute')
expect(configureLink).toHaveClass(
'relative',
'focus-visible:after:ring-2',
'focus-visible:after:ring-state-accent-solid',
'focus-visible:after:ring-inset',
)
expect(configureLink).not.toHaveClass('peer/card-link')
expect(draftCornerLabel && configureLink.contains(draftCornerLabel)).toBe(true)
expect(draftCornerLabel).toHaveClass(
'top-[-0.5px]',
'right-0',
)
expect(draftCornerLabel).not.toHaveClass('z-10', 'z-20')
})
it('only renders the draft badge for unpublished agents', () => {
renderList([
createAgent({
@ -137,4 +200,27 @@ describe('AgentRosterList', () => {
expect(workflowLink).toHaveAttribute('href', '/app/workflow-app-id/workflow')
expect(screen.getByText(/agentV2\.roster\.references\.label/)).toBeInTheDocument()
})
it('duplicates an agent from the card action menu', async () => {
const user = userEvent.setup()
renderList([createAgent()])
await user.click(screen.getByRole('button', { name: /agentV2\.roster\.moreActions/ }))
await user.click(screen.getByRole('menuitem', { name: /common\.operation\.duplicate/ }))
expect(duplicateAgentMutationFn).toHaveBeenCalledWith(
{
params: {
agent_id: 'agent-1',
},
body: {},
},
expect.objectContaining({
client: expect.any(QueryClient),
}),
)
await waitFor(() => {
expect(toast.success).toHaveBeenCalledWith('agentV2.roster.duplicateSuccess')
})
})
})

View File

@ -61,9 +61,10 @@ describe('CreateAgentDialog', () => {
icon_background: '#F5F3FF',
},
}, expect.objectContaining({
onError: expect.any(Function),
onSuccess: expect.any(Function),
}))
const mutationOptions = mutationMock.mutate.mock.calls[0]?.[1]
expect(mutationOptions).not.toHaveProperty('onError')
})
it('shows a field error when creating with an empty name', async () => {

View File

@ -111,9 +111,10 @@ describe('EditAgentDialog', () => {
icon_background: '#F5F3FF',
},
}, expect.objectContaining({
onError: expect.any(Function),
onSuccess: expect.any(Function),
}))
const mutationOptions = mutationMock.mutate.mock.calls[0]?.[1]
expect(mutationOptions).not.toHaveProperty('onError')
})
it('submits the full agent payload when only the role changes', async () => {
@ -138,9 +139,10 @@ describe('EditAgentDialog', () => {
icon_background: '#F5F3FF',
},
}, expect.objectContaining({
onError: expect.any(Function),
onSuccess: expect.any(Function),
}))
const mutationOptions = mutationMock.mutate.mock.calls[0]?.[1]
expect(mutationOptions).not.toHaveProperty('onError')
})
it('submits selected icon fields when the roster icon changes', async () => {
@ -165,9 +167,10 @@ describe('EditAgentDialog', () => {
icon_background: '#E0F2FE',
},
}, expect.objectContaining({
onError: expect.any(Function),
onSuccess: expect.any(Function),
}))
const mutationOptions = mutationMock.mutate.mock.calls[0]?.[1]
expect(mutationOptions).not.toHaveProperty('onError')
})
it('shows a field error when saving with an empty name', async () => {

View File

@ -9,12 +9,15 @@ import {
DropdownMenuSeparator,
DropdownMenuTrigger,
} from '@langgenius/dify-ui/dropdown-menu'
import { toast } from '@langgenius/dify-ui/toast'
import { useMutation } from '@tanstack/react-query'
import { useId, useState } from 'react'
import { useTranslation } from 'react-i18next'
import AppIcon from '@/app/components/base/app-icon'
import { SkeletonRectangle } from '@/app/components/base/skeleton'
import useTimestamp from '@/hooks/use-timestamp'
import Link from '@/next/link'
import { consoleQuery } from '@/service/client'
import { AgentWorkflowReferencesDropdown } from './agent-workflow-references-dropdown'
import { DeleteAgentDialog } from './delete-agent-dialog'
import { EditAgentDialog } from './edit-agent-dialog'
@ -102,6 +105,7 @@ function AgentRosterItem({
const descriptionId = useId()
const [isEditOpen, setIsEditOpen] = useState(false)
const [isDeleteOpen, setIsDeleteOpen] = useState(false)
const duplicateAgentMutation = useMutation(consoleQuery.agent.byAgentId.copy.post.mutationOptions())
const updatedAt = agent.updated_at != null
? formatTime(agent.updated_at, t('roster.dateTimeFormat'))
: null
@ -111,6 +115,24 @@ function AgentRosterItem({
const isDraft = agent.active_config_is_published !== true
const imageUrl = (agent.icon_type === 'image' || agent.icon_type === 'link') ? agent.icon : undefined
const iconType = (imageUrl ? 'image' : agent.icon_type) as AgentIconType | null | undefined
const handleDuplicate = () => {
if (duplicateAgentMutation.isPending)
return
duplicateAgentMutation.mutate({
params: {
agent_id: agent.id,
},
body: {},
}, {
onSuccess: () => {
toast.success(t('roster.duplicateSuccess'))
},
onError: () => {
toast.error(t('roster.duplicateFailed'))
},
})
}
return (
<article className="group relative col-span-1 h-36.5 min-w-0 overflow-hidden rounded-xl border-[0.5px] border-solid border-components-card-border bg-components-card-bg shadow-xs shadow-shadow-shadow-3 transition-shadow duration-200 ease-in-out hover:shadow-lg">
@ -119,7 +141,7 @@ function AgentRosterItem({
href={`/roster/agent/${agent.id}/configure`}
aria-labelledby={nameId}
aria-describedby={agent.description ? descriptionId : undefined}
className="block shrink-0 cursor-pointer touch-manipulation rounded-xl outline-hidden focus-visible:ring-2 focus-visible:ring-state-accent-solid focus-visible:ring-inset"
className="relative block shrink-0 cursor-pointer touch-manipulation rounded-xl outline-hidden after:pointer-events-none after:absolute after:inset-0 after:rounded-xl after:content-[''] focus-visible:after:ring-2 focus-visible:after:ring-state-accent-solid focus-visible:after:ring-inset"
>
<div className="flex items-center gap-3 pt-3.5 pr-4 pb-2 pl-3.5">
<span aria-hidden className="shrink-0">
@ -146,6 +168,14 @@ function AgentRosterItem({
{agent.description}
</div>
</div>
{isDraft && (
<div className="absolute top-[-0.5px] right-0 flex h-5 items-start overflow-hidden">
<div className="h-5 w-3 bg-background-section-burn [clip-path:polygon(0_0,100%_0,100%_100%)]" />
<div className="flex h-5 items-center bg-background-section-burn pr-2 pl-0.5 system-2xs-medium-uppercase text-text-tertiary">
{t('roster.usageStatus.draft')}
</div>
</div>
)}
</Link>
<div className="flex min-w-0 shrink-0 items-center pt-2 pr-3 pb-3 pl-4 system-xs-regular text-text-tertiary">
<div className="flex min-w-0 flex-1 items-center gap-1.5">
@ -172,14 +202,6 @@ function AgentRosterItem({
</div>
</div>
</div>
{isDraft && (
<div className="absolute top-0 right-0 flex h-5 items-start overflow-hidden">
<div className="h-5 w-3 bg-background-section-burn [clip-path:polygon(0_0,100%_0,100%_100%)]" />
<div className="flex h-5 items-center bg-background-section-burn pr-2 pl-0.5 system-2xs-medium-uppercase text-text-tertiary">
{t('roster.usageStatus.draft')}
</div>
</div>
)}
<div
className="pointer-events-none absolute top-2 right-2 z-20 flex items-center overflow-hidden rounded-[10px] border-[0.5px] border-components-actionbar-border bg-components-actionbar-bg p-0.5 opacity-0 shadow-lg backdrop-blur-xs transition-opacity group-focus-within:pointer-events-auto group-focus-within:opacity-100 group-hover:pointer-events-auto group-hover:opacity-100 has-data-popup-open:pointer-events-auto has-data-popup-open:opacity-100"
>
@ -196,7 +218,11 @@ function AgentRosterItem({
<span aria-hidden className="i-ri-edit-line size-4 shrink-0 text-text-tertiary" />
<span>{t('roster.editInfo')}</span>
</DropdownMenuItem>
<DropdownMenuItem disabled className="gap-2">
<DropdownMenuItem
disabled={duplicateAgentMutation.isPending}
className="gap-2"
onClick={handleDuplicate}
>
<span aria-hidden className="i-ri-file-copy-line size-4 shrink-0 text-text-tertiary" />
<span>{tCommon('operation.duplicate')}</span>
</DropdownMenuItem>

View File

@ -70,9 +70,6 @@ export function CreateAgentDialog() {
toast.success(t('roster.createSuccess'))
handleOpenChange(false)
},
onError: () => {
toast.error(t('roster.createFailed'))
},
})
}

View File

@ -136,9 +136,6 @@ export function EditAgentDialog({
toast.success(t('roster.updateSuccess'))
onOpenChange(false)
},
onError: () => {
toast.error(t('roster.updateFailed'))
},
})
}

View File

@ -82,6 +82,7 @@
"agentDetail.configure.files.label": "الملفات",
"agentDetail.configure.files.preview.empty": "لا يوجد محتوى معاينة.",
"agentDetail.configure.files.preview.failed": "فشل تحميل المعاينة.",
"agentDetail.configure.files.preview.unsupported": "هذا الملف لا يدعم المعاينة.",
"agentDetail.configure.files.remove": "إزالة {{name}}",
"agentDetail.configure.files.tip": "الملفات التي يمكن لهذا الوكيل استخدامها أثناء تنسيق المهام.",
"agentDetail.configure.files.toggle": "تبديل الملفات",
@ -241,14 +242,23 @@
"agentDetail.logs.filters.sort.lastCreatedTime": "آخر وقت إنشاء",
"agentDetail.logs.filters.sort.lastUpdatedTime": "آخر وقت تحديث",
"agentDetail.logs.filters.source.all": "المصدر",
"agentDetail.logs.filters.source.empty": "لم يتم العثور على مصادر",
"agentDetail.logs.filters.source.label": "مصدر السجل",
"agentDetail.logs.filters.source.loadFailed": "تعذر تحميل المصادر",
"agentDetail.logs.filters.source.loading": "جارٍ تحميل المصادر…",
"agentDetail.logs.filters.source.searchLabel": "البحث في المصادر",
"agentDetail.logs.filters.source.searchPlaceholder": "بحث",
"agentDetail.logs.filters.source.webapp": "Webapp",
"agentDetail.logs.filters.source.workflow": "سير العمل",
"agentDetail.logs.learnMore": "اعرف المزيد",
"agentDetail.logs.loadFailed": "تعذر تحميل السجلات",
"agentDetail.logs.loading": "جارٍ تحميل السجلات…",
"agentDetail.logs.notAvailable": "غير متاح",
"agentDetail.logs.table.createdTime": "وقت الإنشاء",
"agentDetail.logs.table.endUser": "المستخدم النهائي",
"agentDetail.logs.table.messageCount": "عدد الرسائل",
"agentDetail.logs.table.operationRate": "معدل العمليات",
"agentDetail.logs.table.source": "المصدر",
"agentDetail.logs.table.title": "العنوان",
"agentDetail.logs.table.unread": "غير مقروء",
"agentDetail.logs.table.updatedTime": "وقت التحديث",
@ -324,7 +334,6 @@
"roster.createAgentOptions": "خيارات إنشاء الوكيل",
"roster.createDialog.description": "أنشئ وكيلاً قابلاً لإعادة الاستخدام في Roster مساحة العمل هذه.",
"roster.createDialog.title": "إنشاء وكيل",
"roster.createFailed": "فشل إنشاء الوكيل.",
"roster.createForm.changeIcon": "تغيير أيقونة الوكيل",
"roster.createForm.descriptionLabel": "الوصف",
"roster.createForm.descriptionOptional": "(اختياري)",
@ -341,6 +350,8 @@
"roster.deleteDialog.title": "حذف {{name}}؟",
"roster.deleteFailed": "فشل حذف الوكيل.",
"roster.deleteSuccess": "تم حذف الوكيل.",
"roster.duplicateFailed": "فشل نسخ الوكيل.",
"roster.duplicateSuccess": "تم نسخ الوكيل.",
"roster.editAgent": "تعديل {{name}}",
"roster.editDialog.description": "حدّث اسم Roster والوصف والدور لهذا الوكيل.",
"roster.editDialog.title": "تعديل الوكيل",
@ -376,7 +387,6 @@
"roster.tabs.agent": "وكيل",
"roster.tabs.human": "إنسان",
"roster.tabsLabel": "نوع Roster",
"roster.updateFailed": "فشل تحديث الوكيل. تحقق من الحقول وحاول مرة أخرى.",
"roster.updateSuccess": "تم تحديث الوكيل.",
"roster.usageStatus.draft": "مسودة"
}

View File

@ -82,6 +82,7 @@
"agentDetail.configure.files.label": "Dateien",
"agentDetail.configure.files.preview.empty": "Kein Vorschauinhalt.",
"agentDetail.configure.files.preview.failed": "Vorschau konnte nicht geladen werden.",
"agentDetail.configure.files.preview.unsupported": "Für diese Datei wird keine Vorschau unterstützt.",
"agentDetail.configure.files.remove": "{{name}} entfernen",
"agentDetail.configure.files.tip": "Dateien, die dieser Agent bei der Orchestrierung von Aufgaben verwenden kann.",
"agentDetail.configure.files.toggle": "Dateien umschalten",
@ -241,14 +242,23 @@
"agentDetail.logs.filters.sort.lastCreatedTime": "Zuletzt erstellt",
"agentDetail.logs.filters.sort.lastUpdatedTime": "Zuletzt aktualisiert",
"agentDetail.logs.filters.source.all": "Quelle",
"agentDetail.logs.filters.source.empty": "Keine Quellen gefunden",
"agentDetail.logs.filters.source.label": "Log-Quelle",
"agentDetail.logs.filters.source.loadFailed": "Quellen konnten nicht geladen werden",
"agentDetail.logs.filters.source.loading": "Quellen werden geladen…",
"agentDetail.logs.filters.source.searchLabel": "Quellen suchen",
"agentDetail.logs.filters.source.searchPlaceholder": "Suchen",
"agentDetail.logs.filters.source.webapp": "Webapp",
"agentDetail.logs.filters.source.workflow": "Workflow",
"agentDetail.logs.learnMore": "Mehr erfahren",
"agentDetail.logs.loadFailed": "Logs konnten nicht geladen werden",
"agentDetail.logs.loading": "Logs werden geladen…",
"agentDetail.logs.notAvailable": "Nicht verfügbar",
"agentDetail.logs.table.createdTime": "Erstellungszeit",
"agentDetail.logs.table.endUser": "Endbenutzer",
"agentDetail.logs.table.messageCount": "Nachr. Anzahl",
"agentDetail.logs.table.operationRate": "Operationsrate",
"agentDetail.logs.table.source": "Quelle",
"agentDetail.logs.table.title": "Titel",
"agentDetail.logs.table.unread": "Ungelesen",
"agentDetail.logs.table.updatedTime": "Aktualisierungszeit",
@ -324,7 +334,6 @@
"roster.createAgentOptions": "Optionen zum Erstellen eines Agenten",
"roster.createDialog.description": "Erstellen Sie einen wiederverwendbaren Agenten im Roster dieses Workspaces.",
"roster.createDialog.title": "Agent erstellen",
"roster.createFailed": "Agent konnte nicht erstellt werden.",
"roster.createForm.changeIcon": "Agenten-Symbol ändern",
"roster.createForm.descriptionLabel": "Beschreibung",
"roster.createForm.descriptionOptional": "(optional)",
@ -341,6 +350,8 @@
"roster.deleteDialog.title": "{{name}} löschen?",
"roster.deleteFailed": "Agent konnte nicht gelöscht werden.",
"roster.deleteSuccess": "Agent gelöscht.",
"roster.duplicateFailed": "Agent konnte nicht dupliziert werden.",
"roster.duplicateSuccess": "Agent dupliziert.",
"roster.editAgent": "{{name}} bearbeiten",
"roster.editDialog.description": "Aktualisieren Sie Roster-Name, Beschreibung und Rolle dieses Agenten.",
"roster.editDialog.title": "Agent bearbeiten",
@ -376,7 +387,6 @@
"roster.tabs.agent": "Agent",
"roster.tabs.human": "Mensch",
"roster.tabsLabel": "Roster-Typ",
"roster.updateFailed": "Agent konnte nicht aktualisiert werden. Bitte Felder überprüfen und erneut versuchen.",
"roster.updateSuccess": "Agent aktualisiert.",
"roster.usageStatus.draft": "Entwurf"
}

View File

@ -82,6 +82,7 @@
"agentDetail.configure.files.label": "Files",
"agentDetail.configure.files.preview.empty": "No preview content.",
"agentDetail.configure.files.preview.failed": "Failed to load preview.",
"agentDetail.configure.files.preview.unsupported": "Preview is not supported for this file.",
"agentDetail.configure.files.remove": "Remove {{name}}",
"agentDetail.configure.files.tip": "Files this agent can use while orchestrating tasks.",
"agentDetail.configure.files.toggle": "Toggle files",
@ -241,14 +242,23 @@
"agentDetail.logs.filters.sort.lastCreatedTime": "Last Created time",
"agentDetail.logs.filters.sort.lastUpdatedTime": "Last Updated time",
"agentDetail.logs.filters.source.all": "Source",
"agentDetail.logs.filters.source.empty": "No sources found",
"agentDetail.logs.filters.source.label": "Log source",
"agentDetail.logs.filters.source.loadFailed": "Failed to load sources",
"agentDetail.logs.filters.source.loading": "Loading sources…",
"agentDetail.logs.filters.source.searchLabel": "Search sources",
"agentDetail.logs.filters.source.searchPlaceholder": "Search",
"agentDetail.logs.filters.source.webapp": "Webapp",
"agentDetail.logs.filters.source.workflow": "Workflow",
"agentDetail.logs.learnMore": "Learn more",
"agentDetail.logs.loadFailed": "Failed to load logs",
"agentDetail.logs.loading": "Loading logs…",
"agentDetail.logs.notAvailable": "N/A",
"agentDetail.logs.table.createdTime": "Created Time",
"agentDetail.logs.table.endUser": "End-user",
"agentDetail.logs.table.messageCount": "Msg. Count",
"agentDetail.logs.table.operationRate": "Op. Rate",
"agentDetail.logs.table.source": "Source",
"agentDetail.logs.table.title": "Title",
"agentDetail.logs.table.unread": "Unread",
"agentDetail.logs.table.updatedTime": "Updated Time",
@ -324,7 +334,6 @@
"roster.createAgentOptions": "Create agent options",
"roster.createDialog.description": "Create a reusable agent in this workspace roster.",
"roster.createDialog.title": "Create agent",
"roster.createFailed": "Failed to create agent.",
"roster.createForm.changeIcon": "Change agent icon",
"roster.createForm.descriptionLabel": "Description",
"roster.createForm.descriptionOptional": "(optional)",
@ -341,6 +350,8 @@
"roster.deleteDialog.title": "Delete {{name}}?",
"roster.deleteFailed": "Failed to delete agent.",
"roster.deleteSuccess": "Agent deleted.",
"roster.duplicateFailed": "Failed to duplicate agent.",
"roster.duplicateSuccess": "Agent duplicated.",
"roster.editAgent": "Edit {{name}}",
"roster.editDialog.description": "Update the roster name, description, and role for this agent.",
"roster.editDialog.title": "Edit agent",
@ -376,7 +387,6 @@
"roster.tabs.agent": "Agent",
"roster.tabs.human": "Human",
"roster.tabsLabel": "Roster type",
"roster.updateFailed": "Failed to update agent. Check the fields and try again.",
"roster.updateSuccess": "Agent updated.",
"roster.usageStatus.draft": "Draft"
}

View File

@ -82,6 +82,7 @@
"agentDetail.configure.files.label": "Archivos",
"agentDetail.configure.files.preview.empty": "Sin contenido de vista previa.",
"agentDetail.configure.files.preview.failed": "Error al cargar la vista previa.",
"agentDetail.configure.files.preview.unsupported": "Este archivo no admite vista previa.",
"agentDetail.configure.files.remove": "Eliminar {{name}}",
"agentDetail.configure.files.tip": "Archivos que este agente puede usar al orquestar tareas.",
"agentDetail.configure.files.toggle": "Alternar archivos",
@ -241,14 +242,23 @@
"agentDetail.logs.filters.sort.lastCreatedTime": "Última fecha de creación",
"agentDetail.logs.filters.sort.lastUpdatedTime": "Última fecha de actualización",
"agentDetail.logs.filters.source.all": "Fuente",
"agentDetail.logs.filters.source.empty": "No se encontraron fuentes",
"agentDetail.logs.filters.source.label": "Fuente del registro",
"agentDetail.logs.filters.source.loadFailed": "No se pudieron cargar las fuentes",
"agentDetail.logs.filters.source.loading": "Cargando fuentes…",
"agentDetail.logs.filters.source.searchLabel": "Buscar fuentes",
"agentDetail.logs.filters.source.searchPlaceholder": "Buscar",
"agentDetail.logs.filters.source.webapp": "Webapp",
"agentDetail.logs.filters.source.workflow": "Flujo de trabajo",
"agentDetail.logs.learnMore": "Más información",
"agentDetail.logs.loadFailed": "No se pudieron cargar los registros",
"agentDetail.logs.loading": "Cargando registros…",
"agentDetail.logs.notAvailable": "N/D",
"agentDetail.logs.table.createdTime": "Fecha de creación",
"agentDetail.logs.table.endUser": "Usuario final",
"agentDetail.logs.table.messageCount": "Cant. mens.",
"agentDetail.logs.table.operationRate": "Tasa op.",
"agentDetail.logs.table.source": "Fuente",
"agentDetail.logs.table.title": "Título",
"agentDetail.logs.table.unread": "Sin leer",
"agentDetail.logs.table.updatedTime": "Fecha de actualización",
@ -324,7 +334,6 @@
"roster.createAgentOptions": "Opciones para crear agente",
"roster.createDialog.description": "Crea un agente reutilizable en el roster de este workspace.",
"roster.createDialog.title": "Crear agente",
"roster.createFailed": "Error al crear el agente.",
"roster.createForm.changeIcon": "Cambiar icono del agente",
"roster.createForm.descriptionLabel": "Descripción",
"roster.createForm.descriptionOptional": "(opcional)",
@ -341,6 +350,8 @@
"roster.deleteDialog.title": "¿Eliminar {{name}}?",
"roster.deleteFailed": "Error al eliminar el agente.",
"roster.deleteSuccess": "Agente eliminado.",
"roster.duplicateFailed": "Error al duplicar el agente.",
"roster.duplicateSuccess": "Agente duplicado.",
"roster.editAgent": "Editar {{name}}",
"roster.editDialog.description": "Actualiza el nombre, la descripción y el rol del roster para este agente.",
"roster.editDialog.title": "Editar agente",
@ -376,7 +387,6 @@
"roster.tabs.agent": "Agente",
"roster.tabs.human": "Humano",
"roster.tabsLabel": "Tipo de roster",
"roster.updateFailed": "Error al actualizar el agente. Comprueba los campos y vuelve a intentarlo.",
"roster.updateSuccess": "Agente actualizado.",
"roster.usageStatus.draft": "Borrador"
}

View File

@ -82,6 +82,7 @@
"agentDetail.configure.files.label": "فایل‌ها",
"agentDetail.configure.files.preview.empty": "محتوای پیش‌نمایش وجود ندارد.",
"agentDetail.configure.files.preview.failed": "بارگذاری پیش‌نمایش ناموفق بود.",
"agentDetail.configure.files.preview.unsupported": "این فایل از پیش‌نمایش پشتیبانی نمی‌کند.",
"agentDetail.configure.files.remove": "حذف {{name}}",
"agentDetail.configure.files.tip": "فایل‌هایی که این عامل هنگام هماهنگی وظایف می‌تواند استفاده کند.",
"agentDetail.configure.files.toggle": "تغییر فایل‌ها",
@ -241,14 +242,23 @@
"agentDetail.logs.filters.sort.lastCreatedTime": "آخرین زمان ایجاد",
"agentDetail.logs.filters.sort.lastUpdatedTime": "آخرین زمان به‌روزرسانی",
"agentDetail.logs.filters.source.all": "منبع",
"agentDetail.logs.filters.source.empty": "منبعی پیدا نشد",
"agentDetail.logs.filters.source.label": "منبع گزارش",
"agentDetail.logs.filters.source.loadFailed": "بارگیری منابع ناموفق بود",
"agentDetail.logs.filters.source.loading": "در حال بارگیری منابع…",
"agentDetail.logs.filters.source.searchLabel": "جستجوی منابع",
"agentDetail.logs.filters.source.searchPlaceholder": "جستجو",
"agentDetail.logs.filters.source.webapp": "Webapp",
"agentDetail.logs.filters.source.workflow": "گردش کار",
"agentDetail.logs.learnMore": "اطلاعات بیشتر",
"agentDetail.logs.loadFailed": "بارگیری گزارش‌ها ناموفق بود",
"agentDetail.logs.loading": "در حال بارگیری گزارش‌ها…",
"agentDetail.logs.notAvailable": "ناموجود",
"agentDetail.logs.table.createdTime": "زمان ایجاد",
"agentDetail.logs.table.endUser": "کاربر نهایی",
"agentDetail.logs.table.messageCount": "تعداد پیام",
"agentDetail.logs.table.operationRate": "نرخ عملیات",
"agentDetail.logs.table.source": "منبع",
"agentDetail.logs.table.title": "عنوان",
"agentDetail.logs.table.unread": "خوانده‌نشده",
"agentDetail.logs.table.updatedTime": "زمان به‌روزرسانی",
@ -324,7 +334,6 @@
"roster.createAgentOptions": "گزینه‌های ایجاد عامل",
"roster.createDialog.description": "یک عامل قابل استفاده مجدد در Roster این فضای کاری ایجاد کنید.",
"roster.createDialog.title": "ایجاد عامل",
"roster.createFailed": "ایجاد عامل ناموفق بود.",
"roster.createForm.changeIcon": "تغییر آیکون عامل",
"roster.createForm.descriptionLabel": "توضیحات",
"roster.createForm.descriptionOptional": "(اختیاری)",
@ -341,6 +350,8 @@
"roster.deleteDialog.title": "حذف {{name}}؟",
"roster.deleteFailed": "حذف عامل ناموفق بود.",
"roster.deleteSuccess": "عامل حذف شد.",
"roster.duplicateFailed": "تکثیر عامل ناموفق بود.",
"roster.duplicateSuccess": "عامل تکثیر شد.",
"roster.editAgent": "ویرایش {{name}}",
"roster.editDialog.description": "نام، توضیحات و نقش Roster این عامل را به‌روزرسانی کنید.",
"roster.editDialog.title": "ویرایش عامل",
@ -376,7 +387,6 @@
"roster.tabs.agent": "عامل",
"roster.tabs.human": "انسان",
"roster.tabsLabel": "نوع Roster",
"roster.updateFailed": "به‌روزرسانی عامل ناموفق بود. فیلدها را بررسی کنید و دوباره امتحان کنید.",
"roster.updateSuccess": "عامل به‌روزرسانی شد.",
"roster.usageStatus.draft": "پیش‌نویس"
}

View File

@ -82,6 +82,7 @@
"agentDetail.configure.files.label": "Fichiers",
"agentDetail.configure.files.preview.empty": "Aucun contenu daperçu.",
"agentDetail.configure.files.preview.failed": "Échec du chargement de laperçu.",
"agentDetail.configure.files.preview.unsupported": "Ce fichier ne prend pas en charge laperçu.",
"agentDetail.configure.files.remove": "Supprimer {{name}}",
"agentDetail.configure.files.tip": "Fichiers que cet agent peut utiliser lors de lorchestration des tâches.",
"agentDetail.configure.files.toggle": "Basculer les fichiers",
@ -241,14 +242,23 @@
"agentDetail.logs.filters.sort.lastCreatedTime": "Dernière date de création",
"agentDetail.logs.filters.sort.lastUpdatedTime": "Dernière date de mise à jour",
"agentDetail.logs.filters.source.all": "Source",
"agentDetail.logs.filters.source.empty": "Aucune source trouvée",
"agentDetail.logs.filters.source.label": "Source du journal",
"agentDetail.logs.filters.source.loadFailed": "Échec du chargement des sources",
"agentDetail.logs.filters.source.loading": "Chargement des sources…",
"agentDetail.logs.filters.source.searchLabel": "Rechercher des sources",
"agentDetail.logs.filters.source.searchPlaceholder": "Rechercher",
"agentDetail.logs.filters.source.webapp": "Webapp",
"agentDetail.logs.filters.source.workflow": "Workflow",
"agentDetail.logs.learnMore": "En savoir plus",
"agentDetail.logs.loadFailed": "Échec du chargement des journaux",
"agentDetail.logs.loading": "Chargement des journaux…",
"agentDetail.logs.notAvailable": "N/D",
"agentDetail.logs.table.createdTime": "Date de création",
"agentDetail.logs.table.endUser": "Utilisateur final",
"agentDetail.logs.table.messageCount": "Nb. msg.",
"agentDetail.logs.table.operationRate": "Taux dop.",
"agentDetail.logs.table.source": "Source",
"agentDetail.logs.table.title": "Titre",
"agentDetail.logs.table.unread": "Non lu",
"agentDetail.logs.table.updatedTime": "Date de mise à jour",
@ -324,7 +334,6 @@
"roster.createAgentOptions": "Options de création dagent",
"roster.createDialog.description": "Créez un agent réutilisable dans le roster de cet espace de travail.",
"roster.createDialog.title": "Créer un agent",
"roster.createFailed": "Échec de la création de lagent.",
"roster.createForm.changeIcon": "Changer licône de lagent",
"roster.createForm.descriptionLabel": "Description",
"roster.createForm.descriptionOptional": "(facultatif)",
@ -341,6 +350,8 @@
"roster.deleteDialog.title": "Supprimer {{name}} ?",
"roster.deleteFailed": "Échec de la suppression de lagent.",
"roster.deleteSuccess": "Agent supprimé.",
"roster.duplicateFailed": "Échec de la duplication de lagent.",
"roster.duplicateSuccess": "Agent dupliqué.",
"roster.editAgent": "Modifier {{name}}",
"roster.editDialog.description": "Mettez à jour le nom, la description et le rôle de cet agent dans le roster.",
"roster.editDialog.title": "Modifier lagent",
@ -376,7 +387,6 @@
"roster.tabs.agent": "Agent",
"roster.tabs.human": "Humain",
"roster.tabsLabel": "Type de roster",
"roster.updateFailed": "Échec de la mise à jour de lagent. Vérifiez les champs et réessayez.",
"roster.updateSuccess": "Agent mis à jour.",
"roster.usageStatus.draft": "Brouillon"
}

View File

@ -82,6 +82,7 @@
"agentDetail.configure.files.label": "फ़ाइलें",
"agentDetail.configure.files.preview.empty": "कोई पूर्वावलोकन सामग्री नहीं।",
"agentDetail.configure.files.preview.failed": "पूर्वावलोकन लोड करने में विफल।",
"agentDetail.configure.files.preview.unsupported": "यह फ़ाइल पूर्वावलोकन का समर्थन नहीं करती।",
"agentDetail.configure.files.remove": "{{name}} हटाएँ",
"agentDetail.configure.files.tip": "इस एजेंट द्वारा कार्यों को व्यवस्थित करते समय उपयोग की जा सकने वाली फ़ाइलें।",
"agentDetail.configure.files.toggle": "फ़ाइलें टॉगल करें",
@ -241,14 +242,23 @@
"agentDetail.logs.filters.sort.lastCreatedTime": "अंतिम निर्मित समय",
"agentDetail.logs.filters.sort.lastUpdatedTime": "अंतिम अद्यतन समय",
"agentDetail.logs.filters.source.all": "स्रोत",
"agentDetail.logs.filters.source.empty": "कोई स्रोत नहीं मिला",
"agentDetail.logs.filters.source.label": "लॉग स्रोत",
"agentDetail.logs.filters.source.loadFailed": "स्रोत लोड नहीं हो सके",
"agentDetail.logs.filters.source.loading": "स्रोत लोड हो रहे हैं…",
"agentDetail.logs.filters.source.searchLabel": "स्रोत खोजें",
"agentDetail.logs.filters.source.searchPlaceholder": "खोजें",
"agentDetail.logs.filters.source.webapp": "Webapp",
"agentDetail.logs.filters.source.workflow": "वर्कफ़्लो",
"agentDetail.logs.learnMore": "और अधिक जानें",
"agentDetail.logs.loadFailed": "लॉग लोड नहीं हो सके",
"agentDetail.logs.loading": "लॉग लोड हो रहे हैं…",
"agentDetail.logs.notAvailable": "उपलब्ध नहीं",
"agentDetail.logs.table.createdTime": "निर्मित समय",
"agentDetail.logs.table.endUser": "अंतिम-उपयोगकर्ता",
"agentDetail.logs.table.messageCount": "संदेश संख्या",
"agentDetail.logs.table.operationRate": "ऑपरेशन दर",
"agentDetail.logs.table.source": "स्रोत",
"agentDetail.logs.table.title": "शीर्षक",
"agentDetail.logs.table.unread": "अपठित",
"agentDetail.logs.table.updatedTime": "अद्यतन समय",
@ -324,7 +334,6 @@
"roster.createAgentOptions": "एजेंट बनाने के विकल्प",
"roster.createDialog.description": "इस कार्यक्षेत्र Roster में एक पुनः उपयोग योग्य एजेंट बनाएँ।",
"roster.createDialog.title": "एजेंट बनाएँ",
"roster.createFailed": "एजेंट बनाने में विफल।",
"roster.createForm.changeIcon": "एजेंट आइकन बदलें",
"roster.createForm.descriptionLabel": "विवरण",
"roster.createForm.descriptionOptional": "(वैकल्पिक)",
@ -341,6 +350,8 @@
"roster.deleteDialog.title": "{{name}} हटाएँ?",
"roster.deleteFailed": "एजेंट हटाने में विफल।",
"roster.deleteSuccess": "एजेंट हटा दिया गया।",
"roster.duplicateFailed": "एजेंट डुप्लिकेट करने में विफल।",
"roster.duplicateSuccess": "एजेंट डुप्लिकेट किया गया।",
"roster.editAgent": "{{name}} संपादित करें",
"roster.editDialog.description": "इस एजेंट का Roster नाम, विवरण और भूमिका अपडेट करें।",
"roster.editDialog.title": "एजेंट संपादित करें",
@ -376,7 +387,6 @@
"roster.tabs.agent": "एजेंट",
"roster.tabs.human": "मानव",
"roster.tabsLabel": "Roster प्रकार",
"roster.updateFailed": "एजेंट अपडेट करने में विफल। फ़ील्ड जाँचें और पुनः प्रयास करें।",
"roster.updateSuccess": "एजेंट अपडेट हो गया।",
"roster.usageStatus.draft": "ड्राफ्ट"
}

View File

@ -82,6 +82,7 @@
"agentDetail.configure.files.label": "File",
"agentDetail.configure.files.preview.empty": "Tidak ada konten pratinjau.",
"agentDetail.configure.files.preview.failed": "Gagal memuat pratinjau.",
"agentDetail.configure.files.preview.unsupported": "File ini tidak mendukung pratinjau.",
"agentDetail.configure.files.remove": "Hapus {{name}}",
"agentDetail.configure.files.tip": "File yang dapat digunakan agen ini saat mengorkestrasi tugas.",
"agentDetail.configure.files.toggle": "Alihkan file",
@ -241,14 +242,23 @@
"agentDetail.logs.filters.sort.lastCreatedTime": "Waktu dibuat terakhir",
"agentDetail.logs.filters.sort.lastUpdatedTime": "Waktu diperbarui terakhir",
"agentDetail.logs.filters.source.all": "Sumber",
"agentDetail.logs.filters.source.empty": "Tidak ada sumber ditemukan",
"agentDetail.logs.filters.source.label": "Sumber log",
"agentDetail.logs.filters.source.loadFailed": "Gagal memuat sumber",
"agentDetail.logs.filters.source.loading": "Memuat sumber…",
"agentDetail.logs.filters.source.searchLabel": "Cari sumber",
"agentDetail.logs.filters.source.searchPlaceholder": "Cari",
"agentDetail.logs.filters.source.webapp": "Webapp",
"agentDetail.logs.filters.source.workflow": "Alur kerja",
"agentDetail.logs.learnMore": "Pelajari lebih lanjut",
"agentDetail.logs.loadFailed": "Gagal memuat log",
"agentDetail.logs.loading": "Memuat log…",
"agentDetail.logs.notAvailable": "Tidak tersedia",
"agentDetail.logs.table.createdTime": "Waktu Dibuat",
"agentDetail.logs.table.endUser": "Pengguna akhir",
"agentDetail.logs.table.messageCount": "Jumlah Pesan",
"agentDetail.logs.table.operationRate": "Tingkat Operasi",
"agentDetail.logs.table.source": "Sumber",
"agentDetail.logs.table.title": "Judul",
"agentDetail.logs.table.unread": "Belum dibaca",
"agentDetail.logs.table.updatedTime": "Waktu Diperbarui",
@ -324,7 +334,6 @@
"roster.createAgentOptions": "Opsi buat agen",
"roster.createDialog.description": "Buat agen yang dapat digunakan kembali di Roster ruang kerja ini.",
"roster.createDialog.title": "Buat agen",
"roster.createFailed": "Gagal membuat agen.",
"roster.createForm.changeIcon": "Ubah ikon agen",
"roster.createForm.descriptionLabel": "Deskripsi",
"roster.createForm.descriptionOptional": "(opsional)",
@ -341,6 +350,8 @@
"roster.deleteDialog.title": "Hapus {{name}}?",
"roster.deleteFailed": "Gagal menghapus agen.",
"roster.deleteSuccess": "Agen dihapus.",
"roster.duplicateFailed": "Gagal menduplikasi agen.",
"roster.duplicateSuccess": "Agen diduplikasi.",
"roster.editAgent": "Edit {{name}}",
"roster.editDialog.description": "Perbarui nama, deskripsi, dan peran Roster untuk agen ini.",
"roster.editDialog.title": "Edit agen",
@ -376,7 +387,6 @@
"roster.tabs.agent": "Agen",
"roster.tabs.human": "Manusia",
"roster.tabsLabel": "Tipe Roster",
"roster.updateFailed": "Gagal memperbarui agen. Periksa field dan coba lagi.",
"roster.updateSuccess": "Agen diperbarui.",
"roster.usageStatus.draft": "Draf"
}

View File

@ -82,6 +82,7 @@
"agentDetail.configure.files.label": "File",
"agentDetail.configure.files.preview.empty": "Nessun contenuto in anteprima.",
"agentDetail.configure.files.preview.failed": "Impossibile caricare lanteprima.",
"agentDetail.configure.files.preview.unsupported": "Questo file non supporta lanteprima.",
"agentDetail.configure.files.remove": "Rimuovi {{name}}",
"agentDetail.configure.files.tip": "File che questo agente può utilizzare durante lorchestrazione delle attività.",
"agentDetail.configure.files.toggle": "Attiva/disattiva file",
@ -241,14 +242,23 @@
"agentDetail.logs.filters.sort.lastCreatedTime": "Ultima ora di creazione",
"agentDetail.logs.filters.sort.lastUpdatedTime": "Ultima ora di aggiornamento",
"agentDetail.logs.filters.source.all": "Origine",
"agentDetail.logs.filters.source.empty": "Nessuna fonte trovata",
"agentDetail.logs.filters.source.label": "Origine del log",
"agentDetail.logs.filters.source.loadFailed": "Impossibile caricare le fonti",
"agentDetail.logs.filters.source.loading": "Caricamento fonti…",
"agentDetail.logs.filters.source.searchLabel": "Cerca fonti",
"agentDetail.logs.filters.source.searchPlaceholder": "Cerca",
"agentDetail.logs.filters.source.webapp": "Webapp",
"agentDetail.logs.filters.source.workflow": "Workflow",
"agentDetail.logs.learnMore": "Scopri di più",
"agentDetail.logs.loadFailed": "Impossibile caricare i log",
"agentDetail.logs.loading": "Caricamento log…",
"agentDetail.logs.notAvailable": "N/D",
"agentDetail.logs.table.createdTime": "Ora di creazione",
"agentDetail.logs.table.endUser": "Utente finale",
"agentDetail.logs.table.messageCount": "N. msg.",
"agentDetail.logs.table.operationRate": "Tasso op.",
"agentDetail.logs.table.source": "Fonte",
"agentDetail.logs.table.title": "Titolo",
"agentDetail.logs.table.unread": "Non letto",
"agentDetail.logs.table.updatedTime": "Ora di aggiornamento",
@ -324,7 +334,6 @@
"roster.createAgentOptions": "Opzioni di creazione agente",
"roster.createDialog.description": "Crea un agente riutilizzabile nel roster di questo workspace.",
"roster.createDialog.title": "Crea agente",
"roster.createFailed": "Impossibile creare lagente.",
"roster.createForm.changeIcon": "Cambia icona dellagente",
"roster.createForm.descriptionLabel": "Descrizione",
"roster.createForm.descriptionOptional": "(facoltativo)",
@ -341,6 +350,8 @@
"roster.deleteDialog.title": "Eliminare {{name}}?",
"roster.deleteFailed": "Impossibile eliminare lagente.",
"roster.deleteSuccess": "Agente eliminato.",
"roster.duplicateFailed": "Impossibile duplicare lagente.",
"roster.duplicateSuccess": "Agente duplicato.",
"roster.editAgent": "Modifica {{name}}",
"roster.editDialog.description": "Aggiorna il nome, la descrizione e il ruolo del roster per questo agente.",
"roster.editDialog.title": "Modifica agente",
@ -376,7 +387,6 @@
"roster.tabs.agent": "Agente",
"roster.tabs.human": "Umano",
"roster.tabsLabel": "Tipo di roster",
"roster.updateFailed": "Impossibile aggiornare lagente. Controlla i campi e riprova.",
"roster.updateSuccess": "Agente aggiornato.",
"roster.usageStatus.draft": "Bozza"
}

View File

@ -82,6 +82,7 @@
"agentDetail.configure.files.label": "ファイル",
"agentDetail.configure.files.preview.empty": "プレビュー内容はありません。",
"agentDetail.configure.files.preview.failed": "プレビューの読み込みに失敗しました。",
"agentDetail.configure.files.preview.unsupported": "ファイルはプレビューに対応していません。",
"agentDetail.configure.files.remove": "{{name}} を削除",
"agentDetail.configure.files.tip": "このエージェントがタスクのオーケストレーション中に利用できるファイル。",
"agentDetail.configure.files.toggle": "ファイルの表示を切り替え",
@ -241,14 +242,23 @@
"agentDetail.logs.filters.sort.lastCreatedTime": "最終作成日時",
"agentDetail.logs.filters.sort.lastUpdatedTime": "最終更新日時",
"agentDetail.logs.filters.source.all": "ソース",
"agentDetail.logs.filters.source.empty": "ソースが見つかりません",
"agentDetail.logs.filters.source.label": "ログソース",
"agentDetail.logs.filters.source.loadFailed": "ソースの読み込みに失敗しました",
"agentDetail.logs.filters.source.loading": "ソースを読み込み中…",
"agentDetail.logs.filters.source.searchLabel": "ソースを検索",
"agentDetail.logs.filters.source.searchPlaceholder": "検索",
"agentDetail.logs.filters.source.webapp": "Webapp",
"agentDetail.logs.filters.source.workflow": "ワークフロー",
"agentDetail.logs.learnMore": "詳細を見る",
"agentDetail.logs.loadFailed": "ログの読み込みに失敗しました",
"agentDetail.logs.loading": "ログを読み込み中…",
"agentDetail.logs.notAvailable": "該当なし",
"agentDetail.logs.table.createdTime": "作成日時",
"agentDetail.logs.table.endUser": "エンドユーザー",
"agentDetail.logs.table.messageCount": "メッセージ数",
"agentDetail.logs.table.operationRate": "操作率",
"agentDetail.logs.table.source": "ソース",
"agentDetail.logs.table.title": "タイトル",
"agentDetail.logs.table.unread": "未読",
"agentDetail.logs.table.updatedTime": "更新日時",
@ -324,7 +334,6 @@
"roster.createAgentOptions": "エージェント作成オプション",
"roster.createDialog.description": "このワークスペース Roster に再利用可能なエージェントを作成します。",
"roster.createDialog.title": "エージェントを作成",
"roster.createFailed": "エージェントの作成に失敗しました。",
"roster.createForm.changeIcon": "エージェントアイコンを変更",
"roster.createForm.descriptionLabel": "説明",
"roster.createForm.descriptionOptional": "(任意)",
@ -341,6 +350,8 @@
"roster.deleteDialog.title": "{{name}} を削除しますか?",
"roster.deleteFailed": "エージェントの削除に失敗しました。",
"roster.deleteSuccess": "エージェントを削除しました。",
"roster.duplicateFailed": "エージェントの複製に失敗しました。",
"roster.duplicateSuccess": "エージェントを複製しました。",
"roster.editAgent": "{{name}} を編集",
"roster.editDialog.description": "このエージェントの Roster 名、説明、ロールを更新します。",
"roster.editDialog.title": "エージェントを編集",
@ -376,7 +387,6 @@
"roster.tabs.agent": "エージェント",
"roster.tabs.human": "人間",
"roster.tabsLabel": "Roster タイプ",
"roster.updateFailed": "エージェントの更新に失敗しました。フィールドを確認してもう一度お試しください。",
"roster.updateSuccess": "エージェントを更新しました。",
"roster.usageStatus.draft": "ドラフト"
}

View File

@ -82,6 +82,7 @@
"agentDetail.configure.files.label": "파일",
"agentDetail.configure.files.preview.empty": "미리보기 내용이 없습니다.",
"agentDetail.configure.files.preview.failed": "미리보기를 불러오지 못했습니다.",
"agentDetail.configure.files.preview.unsupported": "이 파일은 미리보기를 지원하지 않습니다.",
"agentDetail.configure.files.remove": "{{name}} 제거",
"agentDetail.configure.files.tip": "이 에이전트가 작업을 오케스트레이션할 때 사용할 수 있는 파일입니다.",
"agentDetail.configure.files.toggle": "파일 전환",
@ -241,14 +242,23 @@
"agentDetail.logs.filters.sort.lastCreatedTime": "마지막 생성 시간",
"agentDetail.logs.filters.sort.lastUpdatedTime": "마지막 업데이트 시간",
"agentDetail.logs.filters.source.all": "소스",
"agentDetail.logs.filters.source.empty": "소스를 찾을 수 없습니다",
"agentDetail.logs.filters.source.label": "로그 소스",
"agentDetail.logs.filters.source.loadFailed": "소스를 불러오지 못했습니다",
"agentDetail.logs.filters.source.loading": "소스를 불러오는 중…",
"agentDetail.logs.filters.source.searchLabel": "소스 검색",
"agentDetail.logs.filters.source.searchPlaceholder": "검색",
"agentDetail.logs.filters.source.webapp": "Webapp",
"agentDetail.logs.filters.source.workflow": "워크플로",
"agentDetail.logs.learnMore": "자세히 알아보기",
"agentDetail.logs.loadFailed": "로그를 불러오지 못했습니다",
"agentDetail.logs.loading": "로그를 불러오는 중…",
"agentDetail.logs.notAvailable": "없음",
"agentDetail.logs.table.createdTime": "생성 시간",
"agentDetail.logs.table.endUser": "최종 사용자",
"agentDetail.logs.table.messageCount": "메시지 수",
"agentDetail.logs.table.operationRate": "작업률",
"agentDetail.logs.table.source": "소스",
"agentDetail.logs.table.title": "제목",
"agentDetail.logs.table.unread": "읽지 않음",
"agentDetail.logs.table.updatedTime": "업데이트 시간",
@ -324,7 +334,6 @@
"roster.createAgentOptions": "에이전트 만들기 옵션",
"roster.createDialog.description": "이 워크스페이스 Roster 에 재사용 가능한 에이전트를 만듭니다.",
"roster.createDialog.title": "에이전트 만들기",
"roster.createFailed": "에이전트 생성에 실패했습니다.",
"roster.createForm.changeIcon": "에이전트 아이콘 변경",
"roster.createForm.descriptionLabel": "설명",
"roster.createForm.descriptionOptional": "(선택 사항)",
@ -341,6 +350,8 @@
"roster.deleteDialog.title": "{{name}}을(를) 삭제하시겠습니까?",
"roster.deleteFailed": "에이전트 삭제에 실패했습니다.",
"roster.deleteSuccess": "에이전트가 삭제되었습니다.",
"roster.duplicateFailed": "에이전트 복제에 실패했습니다.",
"roster.duplicateSuccess": "에이전트가 복제되었습니다.",
"roster.editAgent": "{{name}} 편집",
"roster.editDialog.description": "이 에이전트의 Roster 이름, 설명 및 역할을 업데이트합니다.",
"roster.editDialog.title": "에이전트 편집",
@ -376,7 +387,6 @@
"roster.tabs.agent": "에이전트",
"roster.tabs.human": "사람",
"roster.tabsLabel": "Roster 유형",
"roster.updateFailed": "에이전트 업데이트에 실패했습니다. 필드를 확인 후 다시 시도하세요.",
"roster.updateSuccess": "에이전트가 업데이트되었습니다.",
"roster.usageStatus.draft": "초안"
}

View File

@ -82,6 +82,7 @@
"agentDetail.configure.files.label": "Bestanden",
"agentDetail.configure.files.preview.empty": "Geen voorbeeldinhoud.",
"agentDetail.configure.files.preview.failed": "Laden van voorbeeld mislukt.",
"agentDetail.configure.files.preview.unsupported": "Dit bestand ondersteunt geen voorbeeldweergave.",
"agentDetail.configure.files.remove": "{{name}} verwijderen",
"agentDetail.configure.files.tip": "Bestanden die deze agent kan gebruiken tijdens het orkestreren van taken.",
"agentDetail.configure.files.toggle": "Bestanden in/uit",
@ -241,14 +242,23 @@
"agentDetail.logs.filters.sort.lastCreatedTime": "Laatst aangemaakt",
"agentDetail.logs.filters.sort.lastUpdatedTime": "Laatst bijgewerkt",
"agentDetail.logs.filters.source.all": "Bron",
"agentDetail.logs.filters.source.empty": "Geen bronnen gevonden",
"agentDetail.logs.filters.source.label": "Logbron",
"agentDetail.logs.filters.source.loadFailed": "Bronnen laden mislukt",
"agentDetail.logs.filters.source.loading": "Bronnen laden…",
"agentDetail.logs.filters.source.searchLabel": "Bronnen zoeken",
"agentDetail.logs.filters.source.searchPlaceholder": "Zoeken",
"agentDetail.logs.filters.source.webapp": "Webapp",
"agentDetail.logs.filters.source.workflow": "Workflow",
"agentDetail.logs.learnMore": "Meer informatie",
"agentDetail.logs.loadFailed": "Logboeken laden mislukt",
"agentDetail.logs.loading": "Logboeken laden…",
"agentDetail.logs.notAvailable": "Niet beschikbaar",
"agentDetail.logs.table.createdTime": "Aanmaaktijd",
"agentDetail.logs.table.endUser": "Eindgebruiker",
"agentDetail.logs.table.messageCount": "Aantal ber.",
"agentDetail.logs.table.operationRate": "Actiefrequentie",
"agentDetail.logs.table.source": "Bron",
"agentDetail.logs.table.title": "Titel",
"agentDetail.logs.table.unread": "Ongelezen",
"agentDetail.logs.table.updatedTime": "Bijgewerkte tijd",
@ -324,7 +334,6 @@
"roster.createAgentOptions": "Opties voor agent aanmaken",
"roster.createDialog.description": "Maak een herbruikbare agent in het roster van deze werkruimte.",
"roster.createDialog.title": "Agent aanmaken",
"roster.createFailed": "Aanmaken van agent mislukt.",
"roster.createForm.changeIcon": "Agentpictogram wijzigen",
"roster.createForm.descriptionLabel": "Beschrijving",
"roster.createForm.descriptionOptional": "(optioneel)",
@ -341,6 +350,8 @@
"roster.deleteDialog.title": "{{name}} verwijderen?",
"roster.deleteFailed": "Verwijderen van agent mislukt.",
"roster.deleteSuccess": "Agent verwijderd.",
"roster.duplicateFailed": "Dupliceren van agent mislukt.",
"roster.duplicateSuccess": "Agent gedupliceerd.",
"roster.editAgent": "{{name}} bewerken",
"roster.editDialog.description": "Werk de rosternaam, beschrijving en rol van deze agent bij.",
"roster.editDialog.title": "Agent bewerken",
@ -376,7 +387,6 @@
"roster.tabs.agent": "Agent",
"roster.tabs.human": "Mens",
"roster.tabsLabel": "Rostertype",
"roster.updateFailed": "Bijwerken van agent mislukt. Controleer de velden en probeer het opnieuw.",
"roster.updateSuccess": "Agent bijgewerkt.",
"roster.usageStatus.draft": "Concept"
}

View File

@ -82,6 +82,7 @@
"agentDetail.configure.files.label": "Pliki",
"agentDetail.configure.files.preview.empty": "Brak treści podglądu.",
"agentDetail.configure.files.preview.failed": "Nie udało się załadować podglądu.",
"agentDetail.configure.files.preview.unsupported": "Ten plik nie obsługuje podglądu.",
"agentDetail.configure.files.remove": "Usuń {{name}}",
"agentDetail.configure.files.tip": "Pliki, których ten agent może używać podczas wykonywania zadań.",
"agentDetail.configure.files.toggle": "Przełącz pliki",
@ -241,14 +242,23 @@
"agentDetail.logs.filters.sort.lastCreatedTime": "Ostatnio utworzone",
"agentDetail.logs.filters.sort.lastUpdatedTime": "Ostatnio zaktualizowane",
"agentDetail.logs.filters.source.all": "Źródło",
"agentDetail.logs.filters.source.empty": "Nie znaleziono źródeł",
"agentDetail.logs.filters.source.label": "Źródło logu",
"agentDetail.logs.filters.source.loadFailed": "Nie udało się wczytać źródeł",
"agentDetail.logs.filters.source.loading": "Wczytywanie źródeł…",
"agentDetail.logs.filters.source.searchLabel": "Szukaj źródeł",
"agentDetail.logs.filters.source.searchPlaceholder": "Szukaj",
"agentDetail.logs.filters.source.webapp": "Webapp",
"agentDetail.logs.filters.source.workflow": "Workflow",
"agentDetail.logs.learnMore": "Dowiedz się więcej",
"agentDetail.logs.loadFailed": "Nie udało się wczytać logów",
"agentDetail.logs.loading": "Wczytywanie logów…",
"agentDetail.logs.notAvailable": "Brak danych",
"agentDetail.logs.table.createdTime": "Czas utworzenia",
"agentDetail.logs.table.endUser": "Użytkownik końcowy",
"agentDetail.logs.table.messageCount": "Liczba wiad.",
"agentDetail.logs.table.operationRate": "Współczynnik operacji",
"agentDetail.logs.table.source": "Źródło",
"agentDetail.logs.table.title": "Tytuł",
"agentDetail.logs.table.unread": "Nieprzeczytane",
"agentDetail.logs.table.updatedTime": "Czas aktualizacji",
@ -324,7 +334,6 @@
"roster.createAgentOptions": "Opcje tworzenia agenta",
"roster.createDialog.description": "Utwórz reużywalnego agenta w Roster tego workspace.",
"roster.createDialog.title": "Utwórz agenta",
"roster.createFailed": "Nie udało się utworzyć agenta.",
"roster.createForm.changeIcon": "Zmień ikonę agenta",
"roster.createForm.descriptionLabel": "Opis",
"roster.createForm.descriptionOptional": "(opcjonalnie)",
@ -341,6 +350,8 @@
"roster.deleteDialog.title": "Usunąć {{name}}?",
"roster.deleteFailed": "Nie udało się usunąć agenta.",
"roster.deleteSuccess": "Agent usunięty.",
"roster.duplicateFailed": "Nie udało się zduplikować agenta.",
"roster.duplicateSuccess": "Agent zduplikowany.",
"roster.editAgent": "Edytuj {{name}}",
"roster.editDialog.description": "Zaktualizuj nazwę w Roster, opis i rolę tego agenta.",
"roster.editDialog.title": "Edytuj agenta",
@ -376,7 +387,6 @@
"roster.tabs.agent": "Agent",
"roster.tabs.human": "Człowiek",
"roster.tabsLabel": "Typ Roster",
"roster.updateFailed": "Nie udało się zaktualizować agenta. Sprawdź pola i spróbuj ponownie.",
"roster.updateSuccess": "Agent zaktualizowany.",
"roster.usageStatus.draft": "Wersja robocza"
}

View File

@ -82,6 +82,7 @@
"agentDetail.configure.files.label": "Arquivos",
"agentDetail.configure.files.preview.empty": "Sem conteúdo de pré-visualização.",
"agentDetail.configure.files.preview.failed": "Falha ao carregar a pré-visualização.",
"agentDetail.configure.files.preview.unsupported": "Este arquivo não oferece suporte à pré-visualização.",
"agentDetail.configure.files.remove": "Remover {{name}}",
"agentDetail.configure.files.tip": "Arquivos que este agente pode usar ao orquestrar tarefas.",
"agentDetail.configure.files.toggle": "Alternar arquivos",
@ -241,14 +242,23 @@
"agentDetail.logs.filters.sort.lastCreatedTime": "Última hora de criação",
"agentDetail.logs.filters.sort.lastUpdatedTime": "Última hora de atualização",
"agentDetail.logs.filters.source.all": "Origem",
"agentDetail.logs.filters.source.empty": "Nenhuma fonte encontrada",
"agentDetail.logs.filters.source.label": "Origem do log",
"agentDetail.logs.filters.source.loadFailed": "Falha ao carregar fontes",
"agentDetail.logs.filters.source.loading": "Carregando fontes…",
"agentDetail.logs.filters.source.searchLabel": "Pesquisar fontes",
"agentDetail.logs.filters.source.searchPlaceholder": "Pesquisar",
"agentDetail.logs.filters.source.webapp": "Webapp",
"agentDetail.logs.filters.source.workflow": "Workflow",
"agentDetail.logs.learnMore": "Saiba mais",
"agentDetail.logs.loadFailed": "Falha ao carregar logs",
"agentDetail.logs.loading": "Carregando logs…",
"agentDetail.logs.notAvailable": "N/D",
"agentDetail.logs.table.createdTime": "Hora de criação",
"agentDetail.logs.table.endUser": "Usuário final",
"agentDetail.logs.table.messageCount": "Qtd. msg.",
"agentDetail.logs.table.operationRate": "Taxa de op.",
"agentDetail.logs.table.source": "Fonte",
"agentDetail.logs.table.title": "Título",
"agentDetail.logs.table.unread": "Não lido",
"agentDetail.logs.table.updatedTime": "Hora de atualização",
@ -324,7 +334,6 @@
"roster.createAgentOptions": "Opções de criação de agente",
"roster.createDialog.description": "Crie um agente reutilizável no roster deste workspace.",
"roster.createDialog.title": "Criar agente",
"roster.createFailed": "Falha ao criar o agente.",
"roster.createForm.changeIcon": "Alterar ícone do agente",
"roster.createForm.descriptionLabel": "Descrição",
"roster.createForm.descriptionOptional": "(opcional)",
@ -341,6 +350,8 @@
"roster.deleteDialog.title": "Excluir {{name}}?",
"roster.deleteFailed": "Falha ao excluir o agente.",
"roster.deleteSuccess": "Agente excluído.",
"roster.duplicateFailed": "Falha ao duplicar o agente.",
"roster.duplicateSuccess": "Agente duplicado.",
"roster.editAgent": "Editar {{name}}",
"roster.editDialog.description": "Atualize o nome, a descrição e a função deste agente no roster.",
"roster.editDialog.title": "Editar agente",
@ -376,7 +387,6 @@
"roster.tabs.agent": "Agente",
"roster.tabs.human": "Humano",
"roster.tabsLabel": "Tipo de roster",
"roster.updateFailed": "Falha ao atualizar o agente. Verifique os campos e tente novamente.",
"roster.updateSuccess": "Agente atualizado.",
"roster.usageStatus.draft": "Rascunho"
}

View File

@ -82,6 +82,7 @@
"agentDetail.configure.files.label": "Fișiere",
"agentDetail.configure.files.preview.empty": "Niciun conținut de previzualizat.",
"agentDetail.configure.files.preview.failed": "Încărcarea previzualizării a eșuat.",
"agentDetail.configure.files.preview.unsupported": "Acest fișier nu acceptă previzualizarea.",
"agentDetail.configure.files.remove": "Elimină {{name}}",
"agentDetail.configure.files.tip": "Fișiere pe care acest agent le poate folosi în timpul orchestrării sarcinilor.",
"agentDetail.configure.files.toggle": "Comută fișierele",
@ -241,14 +242,23 @@
"agentDetail.logs.filters.sort.lastCreatedTime": "Ultima dată de creare",
"agentDetail.logs.filters.sort.lastUpdatedTime": "Ultima dată de actualizare",
"agentDetail.logs.filters.source.all": "Sursă",
"agentDetail.logs.filters.source.empty": "Nu s-au găsit surse",
"agentDetail.logs.filters.source.label": "Sursa jurnalului",
"agentDetail.logs.filters.source.loadFailed": "Nu s-au putut încărca sursele",
"agentDetail.logs.filters.source.loading": "Se încarcă sursele…",
"agentDetail.logs.filters.source.searchLabel": "Caută surse",
"agentDetail.logs.filters.source.searchPlaceholder": "Caută",
"agentDetail.logs.filters.source.webapp": "Webapp",
"agentDetail.logs.filters.source.workflow": "Workflow",
"agentDetail.logs.learnMore": "Aflați mai multe",
"agentDetail.logs.loadFailed": "Nu s-au putut încărca jurnalele",
"agentDetail.logs.loading": "Se încarcă jurnalele…",
"agentDetail.logs.notAvailable": "Indisponibil",
"agentDetail.logs.table.createdTime": "Data creării",
"agentDetail.logs.table.endUser": "Utilizator final",
"agentDetail.logs.table.messageCount": "Nr. mesaje",
"agentDetail.logs.table.operationRate": "Rată op.",
"agentDetail.logs.table.source": "Sursă",
"agentDetail.logs.table.title": "Titlu",
"agentDetail.logs.table.unread": "Necitit",
"agentDetail.logs.table.updatedTime": "Data actualizării",
@ -324,7 +334,6 @@
"roster.createAgentOptions": "Opțiuni de creare agent",
"roster.createDialog.description": "Creați un agent reutilizabil în roster-ul acestui workspace.",
"roster.createDialog.title": "Creează agent",
"roster.createFailed": "Crearea agentului a eșuat.",
"roster.createForm.changeIcon": "Schimbă pictograma agentului",
"roster.createForm.descriptionLabel": "Descriere",
"roster.createForm.descriptionOptional": "(opțional)",
@ -341,6 +350,8 @@
"roster.deleteDialog.title": "Ștergeți {{name}}?",
"roster.deleteFailed": "Ștergerea agentului a eșuat.",
"roster.deleteSuccess": "Agent șters.",
"roster.duplicateFailed": "Duplicarea agentului a eșuat.",
"roster.duplicateSuccess": "Agent duplicat.",
"roster.editAgent": "Editați {{name}}",
"roster.editDialog.description": "Actualizați numele, descrierea și rolul din roster pentru acest agent.",
"roster.editDialog.title": "Editați agentul",
@ -376,7 +387,6 @@
"roster.tabs.agent": "Agent",
"roster.tabs.human": "Uman",
"roster.tabsLabel": "Tip de roster",
"roster.updateFailed": "Actualizarea agentului a eșuat. Verificați câmpurile și încercați din nou.",
"roster.updateSuccess": "Agent actualizat.",
"roster.usageStatus.draft": "Ciornă"
}

View File

@ -82,6 +82,7 @@
"agentDetail.configure.files.label": "Файлы",
"agentDetail.configure.files.preview.empty": "Нет содержимого для предпросмотра.",
"agentDetail.configure.files.preview.failed": "Не удалось загрузить предпросмотр.",
"agentDetail.configure.files.preview.unsupported": "Этот файл не поддерживает предварительный просмотр.",
"agentDetail.configure.files.remove": "Удалить {{name}}",
"agentDetail.configure.files.tip": "Файлы, которые этот агент может использовать при выполнении задач.",
"agentDetail.configure.files.toggle": "Переключить файлы",
@ -241,14 +242,23 @@
"agentDetail.logs.filters.sort.lastCreatedTime": "Время последнего создания",
"agentDetail.logs.filters.sort.lastUpdatedTime": "Время последнего обновления",
"agentDetail.logs.filters.source.all": "Источник",
"agentDetail.logs.filters.source.empty": "Источники не найдены",
"agentDetail.logs.filters.source.label": "Источник журнала",
"agentDetail.logs.filters.source.loadFailed": "Не удалось загрузить источники",
"agentDetail.logs.filters.source.loading": "Загрузка источников…",
"agentDetail.logs.filters.source.searchLabel": "Поиск источников",
"agentDetail.logs.filters.source.searchPlaceholder": "Поиск",
"agentDetail.logs.filters.source.webapp": "Webapp",
"agentDetail.logs.filters.source.workflow": "Рабочий процесс",
"agentDetail.logs.learnMore": "Подробнее",
"agentDetail.logs.loadFailed": "Не удалось загрузить журналы",
"agentDetail.logs.loading": "Загрузка журналов…",
"agentDetail.logs.notAvailable": "Н/Д",
"agentDetail.logs.table.createdTime": "Время создания",
"agentDetail.logs.table.endUser": "Конечный пользователь",
"agentDetail.logs.table.messageCount": "Кол-во сообщений",
"agentDetail.logs.table.operationRate": "Уровень операций",
"agentDetail.logs.table.source": "Источник",
"agentDetail.logs.table.title": "Заголовок",
"agentDetail.logs.table.unread": "Непрочитанные",
"agentDetail.logs.table.updatedTime": "Время обновления",
@ -324,7 +334,6 @@
"roster.createAgentOptions": "Варианты создания агента",
"roster.createDialog.description": "Создайте переиспользуемого агента в Roster этого рабочего пространства.",
"roster.createDialog.title": "Создать агента",
"roster.createFailed": "Не удалось создать агента.",
"roster.createForm.changeIcon": "Сменить иконку агента",
"roster.createForm.descriptionLabel": "Описание",
"roster.createForm.descriptionOptional": "(необязательно)",
@ -341,6 +350,8 @@
"roster.deleteDialog.title": "Удалить {{name}}?",
"roster.deleteFailed": "Не удалось удалить агента.",
"roster.deleteSuccess": "Агент удалён.",
"roster.duplicateFailed": "Не удалось продублировать агента.",
"roster.duplicateSuccess": "Агент продублирован.",
"roster.editAgent": "Редактировать {{name}}",
"roster.editDialog.description": "Обновите имя в Roster, описание и роль этого агента.",
"roster.editDialog.title": "Редактировать агента",
@ -376,7 +387,6 @@
"roster.tabs.agent": "Агент",
"roster.tabs.human": "Человек",
"roster.tabsLabel": "Тип Roster",
"roster.updateFailed": "Не удалось обновить агента. Проверьте поля и попробуйте снова.",
"roster.updateSuccess": "Агент обновлён.",
"roster.usageStatus.draft": "Черновик"
}

View File

@ -82,6 +82,7 @@
"agentDetail.configure.files.label": "Datoteke",
"agentDetail.configure.files.preview.empty": "Ni vsebine za predogled.",
"agentDetail.configure.files.preview.failed": "Predogleda ni bilo mogoče naložiti.",
"agentDetail.configure.files.preview.unsupported": "Ta datoteka ne podpira predogleda.",
"agentDetail.configure.files.remove": "Odstrani {{name}}",
"agentDetail.configure.files.tip": "Datoteke, ki jih ta agent lahko uporablja pri izvajanju opravil.",
"agentDetail.configure.files.toggle": "Preklopi datoteke",
@ -241,14 +242,23 @@
"agentDetail.logs.filters.sort.lastCreatedTime": "Čas zadnje izdelave",
"agentDetail.logs.filters.sort.lastUpdatedTime": "Čas zadnje posodobitve",
"agentDetail.logs.filters.source.all": "Vir",
"agentDetail.logs.filters.source.empty": "Ni najdenih virov",
"agentDetail.logs.filters.source.label": "Vir dnevnika",
"agentDetail.logs.filters.source.loadFailed": "Virov ni bilo mogoče naložiti",
"agentDetail.logs.filters.source.loading": "Nalaganje virov…",
"agentDetail.logs.filters.source.searchLabel": "Iskanje virov",
"agentDetail.logs.filters.source.searchPlaceholder": "Iskanje",
"agentDetail.logs.filters.source.webapp": "Webapp",
"agentDetail.logs.filters.source.workflow": "Potek dela",
"agentDetail.logs.learnMore": "Več informacij",
"agentDetail.logs.loadFailed": "Dnevnikov ni bilo mogoče naložiti",
"agentDetail.logs.loading": "Nalaganje dnevnikov…",
"agentDetail.logs.notAvailable": "Ni na voljo",
"agentDetail.logs.table.createdTime": "Čas izdelave",
"agentDetail.logs.table.endUser": "Končni uporabnik",
"agentDetail.logs.table.messageCount": "Št. sporočil",
"agentDetail.logs.table.operationRate": "Stopnja operacij",
"agentDetail.logs.table.source": "Vir",
"agentDetail.logs.table.title": "Naslov",
"agentDetail.logs.table.unread": "Neprebrano",
"agentDetail.logs.table.updatedTime": "Čas posodobitve",
@ -324,7 +334,6 @@
"roster.createAgentOptions": "Možnosti ustvarjanja agenta",
"roster.createDialog.description": "Ustvarite ponovno uporabnega agenta v Roster tega delovnega prostora.",
"roster.createDialog.title": "Ustvari agenta",
"roster.createFailed": "Agenta ni bilo mogoče ustvariti.",
"roster.createForm.changeIcon": "Spremeni ikono agenta",
"roster.createForm.descriptionLabel": "Opis",
"roster.createForm.descriptionOptional": "(neobvezno)",
@ -341,6 +350,8 @@
"roster.deleteDialog.title": "Izbrišem {{name}}?",
"roster.deleteFailed": "Agenta ni bilo mogoče izbrisati.",
"roster.deleteSuccess": "Agent izbrisan.",
"roster.duplicateFailed": "Agenta ni bilo mogoče podvojiti.",
"roster.duplicateSuccess": "Agent podvojen.",
"roster.editAgent": "Uredi {{name}}",
"roster.editDialog.description": "Posodobite ime v Roster, opis in vlogo za tega agenta.",
"roster.editDialog.title": "Uredi agenta",
@ -376,7 +387,6 @@
"roster.tabs.agent": "Agent",
"roster.tabs.human": "Človek",
"roster.tabsLabel": "Tip Roster",
"roster.updateFailed": "Agenta ni bilo mogoče posodobiti. Preverite polja in poskusite znova.",
"roster.updateSuccess": "Agent posodobljen.",
"roster.usageStatus.draft": "Osnutek"
}

View File

@ -82,6 +82,7 @@
"agentDetail.configure.files.label": "ไฟล์",
"agentDetail.configure.files.preview.empty": "ไม่มีเนื้อหาสำหรับแสดงตัวอย่าง",
"agentDetail.configure.files.preview.failed": "โหลดตัวอย่างไม่สำเร็จ",
"agentDetail.configure.files.preview.unsupported": "ไฟล์นี้ไม่รองรับการแสดงตัวอย่าง",
"agentDetail.configure.files.remove": "ลบ {{name}}",
"agentDetail.configure.files.tip": "ไฟล์ที่ตัวแทนนี้สามารถใช้ได้ขณะจัดการงาน",
"agentDetail.configure.files.toggle": "สลับไฟล์",
@ -241,14 +242,23 @@
"agentDetail.logs.filters.sort.lastCreatedTime": "เวลาสร้างล่าสุด",
"agentDetail.logs.filters.sort.lastUpdatedTime": "เวลาอัปเดตล่าสุด",
"agentDetail.logs.filters.source.all": "แหล่งที่มา",
"agentDetail.logs.filters.source.empty": "ไม่พบแหล่งที่มา",
"agentDetail.logs.filters.source.label": "แหล่งที่มาของบันทึก",
"agentDetail.logs.filters.source.loadFailed": "โหลดแหล่งที่มาไม่สำเร็จ",
"agentDetail.logs.filters.source.loading": "กำลังโหลดแหล่งที่มา…",
"agentDetail.logs.filters.source.searchLabel": "ค้นหาแหล่งที่มา",
"agentDetail.logs.filters.source.searchPlaceholder": "ค้นหา",
"agentDetail.logs.filters.source.webapp": "Webapp",
"agentDetail.logs.filters.source.workflow": "เวิร์กโฟลว์",
"agentDetail.logs.learnMore": "เรียนรู้เพิ่มเติม",
"agentDetail.logs.loadFailed": "โหลดบันทึกไม่สำเร็จ",
"agentDetail.logs.loading": "กำลังโหลดบันทึก…",
"agentDetail.logs.notAvailable": "ไม่มีข้อมูล",
"agentDetail.logs.table.createdTime": "เวลาที่สร้าง",
"agentDetail.logs.table.endUser": "ผู้ใช้ปลายทาง",
"agentDetail.logs.table.messageCount": "จำนวนข้อความ",
"agentDetail.logs.table.operationRate": "อัตราการดำเนินการ",
"agentDetail.logs.table.source": "แหล่งที่มา",
"agentDetail.logs.table.title": "ชื่อเรื่อง",
"agentDetail.logs.table.unread": "ยังไม่ได้อ่าน",
"agentDetail.logs.table.updatedTime": "เวลาที่อัปเดต",
@ -324,7 +334,6 @@
"roster.createAgentOptions": "ตัวเลือกการสร้างตัวแทน",
"roster.createDialog.description": "สร้างตัวแทนที่นำกลับมาใช้ใหม่ได้ใน Roster ของเวิร์กสเปซนี้",
"roster.createDialog.title": "สร้างตัวแทน",
"roster.createFailed": "สร้างตัวแทนไม่สำเร็จ",
"roster.createForm.changeIcon": "เปลี่ยนไอคอนตัวแทน",
"roster.createForm.descriptionLabel": "คำอธิบาย",
"roster.createForm.descriptionOptional": "(ตัวเลือก)",
@ -341,6 +350,8 @@
"roster.deleteDialog.title": "ลบ {{name}} หรือไม่",
"roster.deleteFailed": "ลบตัวแทนไม่สำเร็จ",
"roster.deleteSuccess": "ลบตัวแทนแล้ว",
"roster.duplicateFailed": "ทำสำเนาตัวแทนไม่สำเร็จ",
"roster.duplicateSuccess": "ทำสำเนาตัวแทนแล้ว",
"roster.editAgent": "แก้ไข {{name}}",
"roster.editDialog.description": "อัปเดตชื่อ คำอธิบาย และบทบาทใน Roster สำหรับตัวแทนนี้",
"roster.editDialog.title": "แก้ไขตัวแทน",
@ -376,7 +387,6 @@
"roster.tabs.agent": "ตัวแทน",
"roster.tabs.human": "บุคคล",
"roster.tabsLabel": "ประเภท Roster",
"roster.updateFailed": "อัปเดตตัวแทนไม่สำเร็จ ตรวจสอบฟิลด์และลองอีกครั้ง",
"roster.updateSuccess": "อัปเดตตัวแทนแล้ว",
"roster.usageStatus.draft": "ฉบับร่าง"
}

View File

@ -82,6 +82,7 @@
"agentDetail.configure.files.label": "Dosyalar",
"agentDetail.configure.files.preview.empty": "Önizleme içeriği yok.",
"agentDetail.configure.files.preview.failed": "Önizleme yüklenemedi.",
"agentDetail.configure.files.preview.unsupported": "Bu dosya önizlemeyi desteklemiyor.",
"agentDetail.configure.files.remove": "{{name}} kaldır",
"agentDetail.configure.files.tip": "Bu ajanın görevleri koordine ederken kullanabileceği dosyalar.",
"agentDetail.configure.files.toggle": "Dosyaları değiştir",
@ -241,14 +242,23 @@
"agentDetail.logs.filters.sort.lastCreatedTime": "Son Oluşturma zamanı",
"agentDetail.logs.filters.sort.lastUpdatedTime": "Son Güncelleme zamanı",
"agentDetail.logs.filters.source.all": "Kaynak",
"agentDetail.logs.filters.source.empty": "Kaynak bulunamadı",
"agentDetail.logs.filters.source.label": "Günlük kaynağı",
"agentDetail.logs.filters.source.loadFailed": "Kaynaklar yüklenemedi",
"agentDetail.logs.filters.source.loading": "Kaynaklar yükleniyor…",
"agentDetail.logs.filters.source.searchLabel": "Kaynaklarda ara",
"agentDetail.logs.filters.source.searchPlaceholder": "Ara",
"agentDetail.logs.filters.source.webapp": "Webapp",
"agentDetail.logs.filters.source.workflow": "İş akışı",
"agentDetail.logs.learnMore": "Daha fazla bilgi",
"agentDetail.logs.loadFailed": "Günlükler yüklenemedi",
"agentDetail.logs.loading": "Günlükler yükleniyor…",
"agentDetail.logs.notAvailable": "Yok",
"agentDetail.logs.table.createdTime": "Oluşturulma Zamanı",
"agentDetail.logs.table.endUser": "Son kullanıcı",
"agentDetail.logs.table.messageCount": "Mesaj Sayısı",
"agentDetail.logs.table.operationRate": "İşlem Oranı",
"agentDetail.logs.table.source": "Kaynak",
"agentDetail.logs.table.title": "Başlık",
"agentDetail.logs.table.unread": "Okunmamış",
"agentDetail.logs.table.updatedTime": "Güncellenme Zamanı",
@ -324,7 +334,6 @@
"roster.createAgentOptions": "Ajan oluşturma seçenekleri",
"roster.createDialog.description": "Bu çalışma alanı Roster'ında yeniden kullanılabilir bir ajan oluşturun.",
"roster.createDialog.title": "Ajan oluştur",
"roster.createFailed": "Ajan oluşturulamadı.",
"roster.createForm.changeIcon": "Ajan simgesini değiştir",
"roster.createForm.descriptionLabel": "Açıklama",
"roster.createForm.descriptionOptional": "(isteğe bağlı)",
@ -341,6 +350,8 @@
"roster.deleteDialog.title": "{{name}} silinsin mi?",
"roster.deleteFailed": "Ajan silinemedi.",
"roster.deleteSuccess": "Ajan silindi.",
"roster.duplicateFailed": "Ajan çoğaltılamadı.",
"roster.duplicateSuccess": "Ajan çoğaltıldı.",
"roster.editAgent": "{{name}} düzenle",
"roster.editDialog.description": "Bu ajan için Roster adı, açıklaması ve rolünü güncelleyin.",
"roster.editDialog.title": "Ajanı düzenle",
@ -376,7 +387,6 @@
"roster.tabs.agent": "Ajan",
"roster.tabs.human": "İnsan",
"roster.tabsLabel": "Roster türü",
"roster.updateFailed": "Ajan güncellenemedi. Alanları kontrol edip tekrar deneyin.",
"roster.updateSuccess": "Ajan güncellendi.",
"roster.usageStatus.draft": "Taslak"
}

View File

@ -82,6 +82,7 @@
"agentDetail.configure.files.label": "Файли",
"agentDetail.configure.files.preview.empty": "Немає вмісту для перегляду.",
"agentDetail.configure.files.preview.failed": "Не вдалося завантажити перегляд.",
"agentDetail.configure.files.preview.unsupported": "Попередній перегляд цього файлу не підтримується.",
"agentDetail.configure.files.remove": "Видалити {{name}}",
"agentDetail.configure.files.tip": "Файли, які цей агент може використовувати під час оркестрації задач.",
"agentDetail.configure.files.toggle": "Перемкнути файли",
@ -241,14 +242,23 @@
"agentDetail.logs.filters.sort.lastCreatedTime": "Час останнього створення",
"agentDetail.logs.filters.sort.lastUpdatedTime": "Час останнього оновлення",
"agentDetail.logs.filters.source.all": "Джерело",
"agentDetail.logs.filters.source.empty": "Джерела не знайдено",
"agentDetail.logs.filters.source.label": "Джерело журналу",
"agentDetail.logs.filters.source.loadFailed": "Не вдалося завантажити джерела",
"agentDetail.logs.filters.source.loading": "Завантаження джерел…",
"agentDetail.logs.filters.source.searchLabel": "Пошук джерел",
"agentDetail.logs.filters.source.searchPlaceholder": "Пошук",
"agentDetail.logs.filters.source.webapp": "Webapp",
"agentDetail.logs.filters.source.workflow": "Робочий процес",
"agentDetail.logs.learnMore": "Дізнатися більше",
"agentDetail.logs.loadFailed": "Не вдалося завантажити журнали",
"agentDetail.logs.loading": "Завантаження журналів…",
"agentDetail.logs.notAvailable": "Н/Д",
"agentDetail.logs.table.createdTime": "Час створення",
"agentDetail.logs.table.endUser": "Кінцевий користувач",
"agentDetail.logs.table.messageCount": "Кільк. повідомл.",
"agentDetail.logs.table.operationRate": "Рівень операцій",
"agentDetail.logs.table.source": "Джерело",
"agentDetail.logs.table.title": "Заголовок",
"agentDetail.logs.table.unread": "Непрочитані",
"agentDetail.logs.table.updatedTime": "Час оновлення",
@ -324,7 +334,6 @@
"roster.createAgentOptions": "Варіанти створення агента",
"roster.createDialog.description": "Створіть перевикористовного агента в Roster цього робочого простору.",
"roster.createDialog.title": "Створити агента",
"roster.createFailed": "Не вдалося створити агента.",
"roster.createForm.changeIcon": "Змінити іконку агента",
"roster.createForm.descriptionLabel": "Опис",
"roster.createForm.descriptionOptional": "(необов'язково)",
@ -341,6 +350,8 @@
"roster.deleteDialog.title": "Видалити {{name}}?",
"roster.deleteFailed": "Не вдалося видалити агента.",
"roster.deleteSuccess": "Агента видалено.",
"roster.duplicateFailed": "Не вдалося продублювати агента.",
"roster.duplicateSuccess": "Агента продубльовано.",
"roster.editAgent": "Редагувати {{name}}",
"roster.editDialog.description": "Оновіть ім'я в Roster, опис і роль для цього агента.",
"roster.editDialog.title": "Редагувати агента",
@ -376,7 +387,6 @@
"roster.tabs.agent": "Агент",
"roster.tabs.human": "Людина",
"roster.tabsLabel": "Тип Roster",
"roster.updateFailed": "Не вдалося оновити агента. Перевірте поля і спробуйте ще раз.",
"roster.updateSuccess": "Агента оновлено.",
"roster.usageStatus.draft": "Чернетка"
}

View File

@ -82,6 +82,7 @@
"agentDetail.configure.files.label": "Tệp",
"agentDetail.configure.files.preview.empty": "Không có nội dung xem trước.",
"agentDetail.configure.files.preview.failed": "Tải xem trước thất bại.",
"agentDetail.configure.files.preview.unsupported": "Tệp này không hỗ trợ xem trước.",
"agentDetail.configure.files.remove": "Xóa {{name}}",
"agentDetail.configure.files.tip": "Các tệp mà tác nhân này có thể sử dụng khi điều phối tác vụ.",
"agentDetail.configure.files.toggle": "Bật/tắt tệp",
@ -241,14 +242,23 @@
"agentDetail.logs.filters.sort.lastCreatedTime": "Thời gian tạo gần nhất",
"agentDetail.logs.filters.sort.lastUpdatedTime": "Thời gian cập nhật gần nhất",
"agentDetail.logs.filters.source.all": "Nguồn",
"agentDetail.logs.filters.source.empty": "Không tìm thấy nguồn",
"agentDetail.logs.filters.source.label": "Nguồn nhật ký",
"agentDetail.logs.filters.source.loadFailed": "Không tải được nguồn",
"agentDetail.logs.filters.source.loading": "Đang tải nguồn…",
"agentDetail.logs.filters.source.searchLabel": "Tìm kiếm nguồn",
"agentDetail.logs.filters.source.searchPlaceholder": "Tìm kiếm",
"agentDetail.logs.filters.source.webapp": "Webapp",
"agentDetail.logs.filters.source.workflow": "Quy trình làm việc",
"agentDetail.logs.learnMore": "Tìm hiểu thêm",
"agentDetail.logs.loadFailed": "Không tải được nhật ký",
"agentDetail.logs.loading": "Đang tải nhật ký…",
"agentDetail.logs.notAvailable": "Không có",
"agentDetail.logs.table.createdTime": "Thời gian tạo",
"agentDetail.logs.table.endUser": "Người dùng cuối",
"agentDetail.logs.table.messageCount": "Số tin nhắn",
"agentDetail.logs.table.operationRate": "Tỷ lệ thao tác",
"agentDetail.logs.table.source": "Nguồn",
"agentDetail.logs.table.title": "Tiêu đề",
"agentDetail.logs.table.unread": "Chưa đọc",
"agentDetail.logs.table.updatedTime": "Thời gian cập nhật",
@ -324,7 +334,6 @@
"roster.createAgentOptions": "Tùy chọn tạo tác nhân",
"roster.createDialog.description": "Tạo một tác nhân có thể tái sử dụng trong Roster của không gian làm việc này.",
"roster.createDialog.title": "Tạo tác nhân",
"roster.createFailed": "Tạo tác nhân thất bại.",
"roster.createForm.changeIcon": "Đổi biểu tượng tác nhân",
"roster.createForm.descriptionLabel": "Mô tả",
"roster.createForm.descriptionOptional": "(tùy chọn)",
@ -341,6 +350,8 @@
"roster.deleteDialog.title": "Xóa {{name}}?",
"roster.deleteFailed": "Xóa tác nhân thất bại.",
"roster.deleteSuccess": "Đã xóa tác nhân.",
"roster.duplicateFailed": "Nhân bản tác nhân thất bại.",
"roster.duplicateSuccess": "Đã nhân bản tác nhân.",
"roster.editAgent": "Chỉnh sửa {{name}}",
"roster.editDialog.description": "Cập nhật tên, mô tả và vai trò Roster cho tác nhân này.",
"roster.editDialog.title": "Chỉnh sửa tác nhân",
@ -376,7 +387,6 @@
"roster.tabs.agent": "Tác nhân",
"roster.tabs.human": "Con người",
"roster.tabsLabel": "Loại Roster",
"roster.updateFailed": "Cập nhật tác nhân thất bại. Kiểm tra các trường và thử lại.",
"roster.updateSuccess": "Đã cập nhật tác nhân.",
"roster.usageStatus.draft": "Bản nháp"
}

View File

@ -82,6 +82,7 @@
"agentDetail.configure.files.label": "文件",
"agentDetail.configure.files.preview.empty": "暂无预览内容。",
"agentDetail.configure.files.preview.failed": "预览加载失败。",
"agentDetail.configure.files.preview.unsupported": "该文件不支持预览。",
"agentDetail.configure.files.remove": "移除 {{name}}",
"agentDetail.configure.files.tip": "此智能体在编排任务时可使用的文件。",
"agentDetail.configure.files.toggle": "展开或收起文件",
@ -241,14 +242,23 @@
"agentDetail.logs.filters.sort.lastCreatedTime": "最近创建时间",
"agentDetail.logs.filters.sort.lastUpdatedTime": "最近更新时间",
"agentDetail.logs.filters.source.all": "来源",
"agentDetail.logs.filters.source.empty": "暂无来源",
"agentDetail.logs.filters.source.label": "日志来源",
"agentDetail.logs.filters.source.loadFailed": "来源加载失败",
"agentDetail.logs.filters.source.loading": "来源加载中…",
"agentDetail.logs.filters.source.searchLabel": "搜索来源",
"agentDetail.logs.filters.source.searchPlaceholder": "搜索",
"agentDetail.logs.filters.source.webapp": "Webapp",
"agentDetail.logs.filters.source.workflow": "工作流",
"agentDetail.logs.learnMore": "了解更多",
"agentDetail.logs.loadFailed": "日志加载失败",
"agentDetail.logs.loading": "日志加载中…",
"agentDetail.logs.notAvailable": "暂无",
"agentDetail.logs.table.createdTime": "创建时间",
"agentDetail.logs.table.endUser": "终端用户",
"agentDetail.logs.table.messageCount": "消息数",
"agentDetail.logs.table.operationRate": "操作率",
"agentDetail.logs.table.source": "来源",
"agentDetail.logs.table.title": "标题",
"agentDetail.logs.table.unread": "未读",
"agentDetail.logs.table.updatedTime": "更新时间",
@ -324,7 +334,6 @@
"roster.createAgentOptions": "创建智能体选项",
"roster.createDialog.description": "在当前工作区 Roster 中创建一个可复用智能体。",
"roster.createDialog.title": "创建智能体",
"roster.createFailed": "智能体创建失败。",
"roster.createForm.changeIcon": "更换智能体图标",
"roster.createForm.descriptionLabel": "描述",
"roster.createForm.descriptionOptional": "(可选)",
@ -341,6 +350,8 @@
"roster.deleteDialog.title": "删除 {{name}}",
"roster.deleteFailed": "智能体删除失败。",
"roster.deleteSuccess": "智能体已删除。",
"roster.duplicateFailed": "智能体复制失败。",
"roster.duplicateSuccess": "智能体已复制。",
"roster.editAgent": "编辑 {{name}}",
"roster.editDialog.description": "更新此智能体在 Roster 中的名称、描述和角色。",
"roster.editDialog.title": "编辑智能体",
@ -376,7 +387,6 @@
"roster.tabs.agent": "智能体",
"roster.tabs.human": "人工成员",
"roster.tabsLabel": "Roster 类型",
"roster.updateFailed": "智能体更新失败。请检查字段后重试。",
"roster.updateSuccess": "智能体已更新。",
"roster.usageStatus.draft": "草稿"
}

View File

@ -82,6 +82,7 @@
"agentDetail.configure.files.label": "檔案",
"agentDetail.configure.files.preview.empty": "暫無預覽內容。",
"agentDetail.configure.files.preview.failed": "預覽載入失敗。",
"agentDetail.configure.files.preview.unsupported": "此檔案不支援預覽。",
"agentDetail.configure.files.remove": "移除 {{name}}",
"agentDetail.configure.files.tip": "此智能體在編排任務時可使用的檔案。",
"agentDetail.configure.files.toggle": "展開或收合檔案",
@ -241,14 +242,23 @@
"agentDetail.logs.filters.sort.lastCreatedTime": "最近建立時間",
"agentDetail.logs.filters.sort.lastUpdatedTime": "最近更新時間",
"agentDetail.logs.filters.source.all": "來源",
"agentDetail.logs.filters.source.empty": "沒有找到來源",
"agentDetail.logs.filters.source.label": "日誌來源",
"agentDetail.logs.filters.source.loadFailed": "來源載入失敗",
"agentDetail.logs.filters.source.loading": "來源載入中…",
"agentDetail.logs.filters.source.searchLabel": "搜尋來源",
"agentDetail.logs.filters.source.searchPlaceholder": "搜尋",
"agentDetail.logs.filters.source.webapp": "Webapp",
"agentDetail.logs.filters.source.workflow": "工作流程",
"agentDetail.logs.learnMore": "了解更多",
"agentDetail.logs.loadFailed": "日誌載入失敗",
"agentDetail.logs.loading": "日誌載入中…",
"agentDetail.logs.notAvailable": "無",
"agentDetail.logs.table.createdTime": "建立時間",
"agentDetail.logs.table.endUser": "終端使用者",
"agentDetail.logs.table.messageCount": "訊息數",
"agentDetail.logs.table.operationRate": "操作率",
"agentDetail.logs.table.source": "來源",
"agentDetail.logs.table.title": "標題",
"agentDetail.logs.table.unread": "未讀",
"agentDetail.logs.table.updatedTime": "更新時間",
@ -324,7 +334,6 @@
"roster.createAgentOptions": "建立智能體選項",
"roster.createDialog.description": "在目前工作區 Roster 中建立一個可複用智能體。",
"roster.createDialog.title": "建立智能體",
"roster.createFailed": "智能體建立失敗。",
"roster.createForm.changeIcon": "更換智能體圖示",
"roster.createForm.descriptionLabel": "描述",
"roster.createForm.descriptionOptional": "(選用)",
@ -341,6 +350,8 @@
"roster.deleteDialog.title": "刪除 {{name}}",
"roster.deleteFailed": "智能體刪除失敗。",
"roster.deleteSuccess": "智能體已刪除。",
"roster.duplicateFailed": "智能體複製失敗。",
"roster.duplicateSuccess": "智能體已複製。",
"roster.editAgent": "編輯 {{name}}",
"roster.editDialog.description": "更新此智能體在 Roster 中的名稱、描述和角色。",
"roster.editDialog.title": "編輯智能體",
@ -376,7 +387,6 @@
"roster.tabs.agent": "智能體",
"roster.tabs.human": "人工成員",
"roster.tabsLabel": "Roster 類型",
"roster.updateFailed": "智能體更新失敗。請檢查欄位後再試。",
"roster.updateSuccess": "智能體已更新。",
"roster.usageStatus.draft": "草稿"
}

View File

@ -188,6 +188,40 @@ describe('consoleQuery agent mutation defaults', () => {
})
})
it('should cache copied agent detail and invalidate roster lists after copying an agent', async () => {
const consoleQuery = await loadConsoleQuery()
const queryClient = new QueryClient()
const invalidateQueries = vi.spyOn(queryClient, 'invalidateQueries')
const copiedAgent = createAgent({ id: 'copied-agent', name: 'Agent copy' })
const mutationOptions = consoleQuery.agent.byAgentId.copy.post.mutationOptions()
await mutationOptions.onSuccess?.(
copiedAgent,
{
params: {
agent_id: 'source-agent',
},
body: {},
},
undefined,
createMutationContext(queryClient),
)
expect(queryClient.getQueryData(consoleQuery.agent.byAgentId.get.queryKey({
input: {
params: {
agent_id: copiedAgent.id,
},
},
}))).toEqual(copiedAgent)
expect(invalidateQueries).toHaveBeenCalledWith({
queryKey: consoleQuery.agent.get.key(),
})
expect(invalidateQueries).toHaveBeenCalledWith({
queryKey: consoleQuery.agent.inviteOptions.get.key(),
})
})
it('should invalidate invite option lists after updating an agent', async () => {
const consoleQuery = await loadConsoleQuery()
const queryClient = new QueryClient()

View File

@ -361,6 +361,30 @@ export const consoleQuery: RouterUtils<typeof consoleClient> = createTanstackQue
},
},
byAgentId: {
copy: {
post: {
mutationOptions: {
onSuccess: (copiedAgent, _variables, _onMutateResult, context) => {
context.client.setQueryData(
consoleQuery.agent.byAgentId.get.queryKey({
input: {
params: {
agent_id: copiedAgent.id,
},
},
}),
copiedAgent,
)
context.client.invalidateQueries({
queryKey: consoleQuery.agent.get.key(),
})
context.client.invalidateQueries({
queryKey: consoleQuery.agent.inviteOptions.get.key(),
})
},
},
},
},
put: {
mutationOptions: {
onSuccess: (updatedAgent, variables, _onMutateResult, context) => {