feat: add external agent workflow selection and settings

This commit is contained in:
Joel committed 2026-11-11 18:07:32 +08:00
1 parent a8da8bac60
commit f2417f3ec0
58 files changed
+2509 -100

No files matched your search

-13
View File
@@ -3658,11 +3658,6 @@
"count": 2
}
},
"web/app/components/workflow/block-selector/agent-selector.tsx": {
"better-tailwindcss/enforce-logical-properties": {
"count": 7
}
},
"web/app/components/workflow/block-selector/all-start-blocks.tsx": {
"better-tailwindcss/enforce-logical-properties": {
"count": 1
@@ -4133,9 +4128,6 @@
}
},
"web/app/components/workflow/nodes/_base/components/node-control.tsx": {
"better-tailwindcss/enforce-logical-properties": {
"count": 1
},
"jsx-a11y/click-events-have-key-events": {
"count": 1
},
@@ -4386,11 +4378,6 @@
"count": 2
}
},
"web/app/components/workflow/nodes/agent-v2/components/agent-output-variables/index.tsx": {
"better-tailwindcss/enforce-logical-properties": {
"count": 2
}
},
"web/app/components/workflow/nodes/agent/components/model-bar.tsx": {
"better-tailwindcss/enforce-logical-properties": {
"count": 2
@@ -12,6 +12,8 @@ import { createEdge, createNode } from '@/app/components/workflow/__tests__/fixt
import { renderWorkflowCanvasHook } from '@/app/components/workflow/__tests__/workflow-test-env'
import { ChatVarType } from '@/app/components/workflow/panel/chat-variable-panel/type'
import { BlockEnum } from '@/app/components/workflow/types'
import { createExternalAgentWorkflowPreviewData } from '@/features/agent-v2/external-agent/workflow-preview/preview'
import { createExternalRosterPreview } from '@/features/agent-v2/roster/external-roster-preview'
import { markAppDeletionFailed, markAppDeletionStarted } from '@/service/app-deletion'
import { createHttpError } from '@/service/http-error'
import { createConsoleQueryWrapper, seedAppDetail } from '@/test/console/query-data'
@@ -439,6 +441,29 @@ describe('useNodesSyncDraft', () => {
)
})
it('excludes external agent previews and their connected edges from backend draft payloads', async () => {
const previewData = createExternalAgentWorkflowPreviewData(createExternalRosterPreview()[1]!)
const { result } = renderUseNodesSyncDraft({
nodes: [
startNode,
createNode({ id: 'external-preview', data: { type: BlockEnum.Agent, ...previewData } }),
],
edges: [createEdge({ source: startNode.id, target: 'external-preview' })],
})
await act(async () => {
await result.current.doSyncWorkflowDraft()
})
expect(mockSyncWorkflowDraft).toHaveBeenCalledExactlyOnceWith(
expect.objectContaining({
params: expect.objectContaining({
graph: expect.objectContaining({ nodes: [savedStartNode], edges: [] }),
}),
}),
)
})
it('keeps pending inline Agent v2 nodes in draft without incomplete bindings', async () => {
const agentData = {
type: BlockEnum.Agent,
@@ -389,7 +389,7 @@ describe('Blocks', () => {
expect(await screen.findByText('Nadia')).toBeInTheDocument()
expect(screen.getByText('Researcher')).toBeInTheDocument()
await user.click(screen.getByRole('menuitem', { name: 'Nadia Researcher' }))
await user.click(screen.getByRole('button', { name: /Nadia.*Researcher/ }))
expect(onSelect).toHaveBeenCalledWith(BlockEnum.AgentV2, {
agent_binding: {
@@ -511,11 +511,14 @@ describe('Blocks', () => {
await user.click(screen.getByRole('button', { name: /Agent/ }))
expect(await screen.findByText('Nadia')).toBeInTheDocument()
await user.click(screen.getByRole('menuitem', { name: 'Nadia Researcher' }))
await user.click(screen.getByRole('button', { name: /Nadia.*Researcher/ }))
await waitFor(() =>
expect(queryMocks.toastError).toHaveBeenCalledWith('workflow.nodes.agent.modelNotSelected'),
expect(screen.getByRole('button', { name: /Nadia.*Researcher/ })).toHaveAttribute(
'aria-disabled',
'true',
)
expect(screen.getByText('workflow.nodes.agent.modelNotSelected')).toBeInTheDocument()
expect(queryMocks.toastError).not.toHaveBeenCalled()
expect(onSelect).not.toHaveBeenCalled()
})
@@ -546,14 +549,14 @@ describe('Blocks', () => {
)
await user.click(screen.getByRole('button', { name: /Agent/ }))
const consoleLink = await screen.findByRole('menuitem', {
const consoleLink = await screen.findByRole('link', {
name: 'agentRoster.roster.nodeSelector.manageInAgentConsole',
})
expect(consoleLink).toHaveAttribute('href', '/agents')
expect(consoleLink).toHaveAttribute('target', '_blank')
expect(consoleLink).toHaveAttribute('rel', 'noopener noreferrer')
await user.click(
await screen.findByRole('menuitem', {
await screen.findByRole('button', {
name: 'agentRoster.roster.nodeSelector.startFromScratch',
}),
)
@@ -0,0 +1,250 @@
import type { AgentInviteOptionResponse } from '@dify/contracts/api/console/agent/types.gen'
import type { ComponentProps } from 'react'
import type { EditorResource } from '@/app/components/workflow/editor-resource/resource'
import type { NodeDefault } from '@/app/components/workflow/types'
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
import { createInstance } from 'i18next'
import { I18nextProvider } from 'react-i18next'
import { page, userEvent } from 'vite-plus/test/browser'
import { render } from 'vitest-browser-react'
import { EditorResourceScope } from '@/app/components/workflow/editor-resource/provider'
import { BlockEnum } from '@/app/components/workflow/types'
import agentRoster from '@/i18n/locales/en-US/agent-roster.json'
import common from '@/i18n/locales/en-US/common.json'
import navigation from '@/i18n/locales/en-US/navigation.json'
import workflow from '@/i18n/locales/en-US/workflow.json'
import { guardUnexpectedFetch } from '@/test/unexpected-fetch'
import { AgentBlockItem } from '../agent-selector'
import { BlockClassification } from '../types'
const difyAgent: AgentInviteOptionResponse = {
id: 'dify-agent-1',
name: 'Research Agent',
description: 'Researches public information.',
role: 'Researcher',
icon_type: 'emoji',
icon: '🤖',
icon_background: '#FFFFFF',
active_config_snapshot_id: 'snapshot-1',
agent_kind: 'dify_agent',
scope: 'roster',
source: 'workflow',
status: 'active',
}
const mocks = vi.hoisted(() => ({ fetchAgents: vi.fn() }))
vi.mock('react-i18next', async (importOriginal) => importOriginal())
// boundary: Dify invite options are server data; source filtering and selection remain real.
vi.mock('@/service/console', async (importOriginal) => {
const actual = await importOriginal<typeof import('@/service/console')>()
const { withConsoleResponses } = await import('@/test/console/service-responses')
return {
...actual,
consoleQuery: withConsoleResponses(actual.consoleQuery, {
'agent.inviteOptions.get': (
input: Parameters<typeof actual.consoleClient.agent.inviteOptions.get>[0],
) => {
mocks.fetchAgents(input)
const keyword = input?.query?.keyword?.toLowerCase() ?? ''
const data = [difyAgent].filter((agent) => agent.name.toLowerCase().includes(keyword))
return { data, has_more: false, limit: 8, page: 1, total: data.length }
},
}),
}
})
const block: NodeDefault = {
metaData: {
classification: BlockClassification.Default,
sort: 3,
type: BlockEnum.AgentV2,
title: 'Agent',
author: 'Dify',
description: 'Call an agent.',
},
defaultValue: {},
checkValid: () => ({ isValid: true }),
}
type AgentBlockProps = ComponentProps<typeof AgentBlockItem>
async function setup(
resource: EditorResource = {
kind: 'app',
collaborationEnabled: false,
appId: 'workflow-app',
mode: 'workflow',
},
) {
await page.viewport(1100, 750)
const i18n = createInstance()
await i18n.init({
lng: 'en-US',
resources: { 'en-US': { agentRoster, common, navigation, workflow } },
keySeparator: false,
interpolation: { escapeValue: false },
})
const queryClient = new QueryClient({ defaultOptions: { queries: { retry: false } } })
onTestFinished(() => queryClient.clear())
const onSelect = vi.fn<AgentBlockProps['onSelect']>()
const onSelectExternal = vi.fn<NonNullable<AgentBlockProps['onSelectExternal']>>()
const onStartFromScratch = vi.fn()
await render(
<QueryClientProvider client={queryClient}>
<EditorResourceScope resource={resource}>
<I18nextProvider i18n={i18n}>
<div className="isolate w-60 p-4">
<AgentBlockItem
block={block}
onSelect={onSelect}
onSelectExternal={onSelectExternal}
onStartFromScratch={onStartFromScratch}
/>
</div>
</I18nextProvider>
</EditorResourceScope>
</QueryClientProvider>,
)
const trigger = page.getByRole('button', { name: /^Agent/ })
await trigger.click()
const picker = page.getByRole('dialog', { name: 'Select an agent', exact: true })
await expect.element(picker).toBeVisible()
return { picker, trigger, onSelect, onSelectExternal, onStartFromScratch }
}
beforeEach(({ onTestFinished }) => {
mocks.fetchAgents.mockClear()
onTestFinished(guardUnexpectedFetch())
})
it('filters the active source while sharing the search and avoiding Dify requests on External', async () => {
const { picker } = await setup()
const search = picker.getByRole('searchbox', { name: 'Search agents', exact: true })
await expect
.element(picker.getByRole('tab', { name: 'All', exact: true }))
.toHaveAttribute('aria-selected', 'true')
await expect.element(picker.getByRole('button', { name: /Research Agent/ })).toBeVisible()
await expect.element(picker.getByRole('button', { name: /Contract Review Agent/ })).toBeVisible()
await picker.getByRole('tab', { name: 'External', exact: true }).click()
await expect
.element(picker.getByRole('button', { name: /Research Agent/ }))
.not.toBeInTheDocument()
await expect
.element(picker.getByRole('button', { name: 'Start from Scratch', exact: true }))
.not.toBeInTheDocument()
const requests = mocks.fetchAgents.mock.calls.length
vi.useFakeTimers({ toFake: ['Date', 'setTimeout', 'clearTimeout'] })
onTestFinished(() => {
vi.useRealTimers()
})
await search.fill('document')
await expect.element(search).toHaveValue('document')
await vi.advanceTimersByTimeAsync(300)
await expect.element(picker.getByRole('button', { name: /Document Review Agent/ })).toBeVisible()
await expect
.element(picker.getByRole('button', { name: /Contract Review Agent/ }))
.not.toBeInTheDocument()
expect(mocks.fetchAgents).toHaveBeenCalledTimes(requests)
vi.useRealTimers()
await picker.getByRole('tab', { name: 'Dify', exact: true }).click()
await expect
.poll(() => mocks.fetchAgents.mock.calls.at(-1))
.toEqual([{ query: { app_id: 'workflow-app', limit: 8, page: 1, keyword: 'document' } }])
await expect.element(search).toHaveValue('document')
await expect.element(picker.getByText('No matching agents', { exact: true })).toBeVisible()
await picker.getByRole('tab', { name: 'All', exact: true }).click()
await expect.element(picker.getByRole('button', { name: /Document Review Agent/ })).toBeVisible()
})
it('skips disabled external agents during keyboard selection and closes with the external callback', async () => {
const { picker, onSelect, onSelectExternal } = await setup()
await picker.getByRole('tab', { name: 'External', exact: true }).click()
const contract = picker.getByRole('button', { name: /Contract Review Agent/ })
const document = picker.getByRole('button', { name: /Document Review Agent/ })
await expect.element(picker.getByRole('button', { name: /Invoice Audit Agent/ })).toBeDisabled()
await userEvent.tab()
await expect
.element(picker.getByRole('tabpanel', { name: 'External', exact: true }))
.toHaveFocus()
await userEvent.tab()
await expect.element(contract).toHaveFocus()
await userEvent.tab()
await expect.element(document).toHaveFocus()
await userEvent.tab()
const manage = picker.getByRole('link', { name: 'Manage in Agents', exact: true })
await expect.element(manage).toHaveFocus()
await expect.element(manage).toHaveAttribute('href', '/agents')
await userEvent.tab({ shift: true })
await expect.element(document).toHaveFocus()
await userEvent.keyboard('{Enter}')
await expect
.poll(() => onSelectExternal.mock.calls)
.toEqual([[expect.objectContaining({ key: 'document-review', name: 'Document Review Agent' })]])
expect(onSelect).not.toHaveBeenCalled()
await expect.element(picker).not.toBeInTheDocument()
})
it('keeps Dify selection and starting from scratch as distinct actions', async () => {
const { picker, trigger, onSelect, onSelectExternal, onStartFromScratch } = await setup()
await picker.getByRole('button', { name: /Research Agent/ }).click()
await expect
.poll(() => onSelect.mock.calls)
.toEqual([
[
{
description: difyAgent.description,
icon: difyAgent.icon,
icon_background: difyAgent.icon_background,
icon_type: difyAgent.icon_type,
id: difyAgent.id,
name: difyAgent.name,
role: difyAgent.role,
},
],
])
expect(onSelectExternal).not.toHaveBeenCalled()
await expect.element(picker).not.toBeInTheDocument()
await trigger.click()
await picker.getByRole('button', { name: 'Start from Scratch', exact: true }).click()
expect(onStartFromScratch).toHaveBeenCalledOnce()
expect(onSelect).toHaveBeenCalledOnce()
await expect.element(picker).not.toBeInTheDocument()
})
it('resets the source and search when closing and reopening the picker', async () => {
const { picker, trigger } = await setup()
await picker.getByRole('tab', { name: 'External', exact: true }).click()
await picker.getByRole('searchbox', { name: 'Search agents', exact: true }).fill('contract')
await expect
.element(picker.getByRole('button', { name: /Document Review Agent/ }))
.not.toBeInTheDocument()
await userEvent.keyboard('{Escape}')
await expect
.element(picker.getByRole('searchbox', { name: 'Search agents', exact: true }))
.toHaveValue('')
await userEvent.keyboard('{Escape}')
await expect.element(picker).not.toBeInTheDocument()
await trigger.click()
await expect
.element(picker.getByRole('tab', { name: 'All', exact: true }))
.toHaveAttribute('aria-selected', 'true')
await expect
.element(picker.getByRole('searchbox', { name: 'Search agents', exact: true }))
.toHaveValue('')
await expect.element(picker.getByRole('button', { name: /Research Agent/ })).toBeVisible()
await expect.element(picker.getByRole('button', { name: /Document Review Agent/ })).toBeVisible()
})
it('keeps external previews out of snippet selection and omits the workflow app filter', async () => {
const { picker } = await setup({ kind: 'snippet', snippetId: 'snippet-1' })
await expect
.element(picker.getByRole('tab', { name: 'External', exact: true }))
.not.toBeInTheDocument()
await expect
.element(picker.getByRole('button', { name: /Contract Review Agent/ }))
.not.toBeInTheDocument()
await expect
.poll(() => mocks.fetchAgents.mock.calls.at(-1))
.toEqual([{ query: { limit: 8, page: 1 } }])
})
@@ -3,6 +3,7 @@ import type { MenuContentProps } from '@langgenius/dify-ui/menu'
import type { ReactNode } from 'react'
import type { NodeDefault } from '../types'
import type { AgentRosterNodeData } from './types'
import type { ExternalAgentWorkflowPreviewAgent } from '@/features/agent-v2/external-agent/workflow-preview/types'
import { Button } from '@langgenius/dify-ui/button'
import { cn } from '@langgenius/dify-ui/cn'
import {
@@ -17,6 +18,7 @@ import {
MenuSeparator,
MenuTrigger,
} from '@langgenius/dify-ui/menu'
import { Popover, PopoverContent, PopoverTitle, PopoverTrigger } from '@langgenius/dify-ui/popover'
import { keepPreviousData, useQuery } from '@tanstack/react-query'
import { useDebounce } from 'ahooks'
import { useAtomValue } from 'jotai'
@@ -24,11 +26,15 @@ import { createContext, use, useState } from 'react'
import { useTranslation } from 'react-i18next'
import AppIcon from '@/app/components/base/app-icon'
import Badge from '@/app/components/base/badge'
import { editorAppIdAtom } from '@/app/components/workflow/editor-resource/state'
import {
editorAppIdAtom,
editorResourceKindAtom,
} from '@/app/components/workflow/editor-resource/state'
import { toast } from '@/app/notifications'
import Link from '@/next/link'
import { consoleQuery } from '@/service/console'
import BlockIcon from '../block-icon'
import { WorkflowAgentSelectorContent } from './workflow-agent-selector'
const AGENT_SELECTOR_PAGE_SIZE = 8
@@ -163,7 +169,7 @@ function AgentSelectorList({
<MenuItem
key={agent.id}
closeOnClick={Boolean(agent.active_config_snapshot_id)}
className="h-auto gap-2 py-1.5 pr-3 pl-2"
className="h-auto gap-2 py-1.5 ps-2 pe-3"
onClick={() => handleSelect(agent)}
>
<span aria-hidden className="shrink-0">
@@ -215,7 +221,7 @@ function AgentSelectorLoadingSkeleton() {
<div
key={key}
className={cn(
'flex items-center gap-2 py-1.5 pr-3 pl-2 opacity-20',
'flex items-center gap-2 py-1.5 ps-2 pe-3 opacity-20',
index === 3 && 'opacity-10',
)}
>
@@ -247,27 +253,32 @@ function toAgentRosterNodeData(agent: AgentInviteOptionResponse): AgentRosterNod
export function AgentBlockItem({
block,
onSelect,
onSelectExternal,
onStartFromScratch,
}: {
block: NodeDefault
onSelect: (agent: AgentRosterNodeData) => void
onSelectExternal?: (agent: ExternalAgentWorkflowPreviewAgent) => void
onStartFromScratch: () => void
}) {
const { t } = useTranslation(['navigation'])
const { t: tAgentRoster } = useTranslation(['agentRoster'])
const resourceKind = useAtomValue(editorResourceKindAtom)
const [open, setOpen] = useState(false)
return (
<AgentSelectorMenu>
<AgentSelectorMenuTrigger
<Popover open={open} onOpenChange={setOpen}>
<PopoverTrigger
openOnHover
render={
<Button
variant="ghost"
size="medium"
className="w-full justify-start gap-0 px-3 text-left data-popup-open:bg-state-base-hover"
className="w-full justify-start gap-0 px-3 text-start data-popup-open:bg-state-base-hover"
/>
}
>
<BlockIcon className="mr-2 shrink-0" type={block.metaData.type} />
<BlockIcon className="me-2 shrink-0" type={block.metaData.type} />
<span className="min-w-0 grow truncate system-sm-medium text-text-secondary">
{block.metaData.title}
</span>
@@ -275,19 +286,28 @@ export function AgentBlockItem({
size="xs"
variant="dimm"
text={t(($) => $['menus.status'], { ns: 'navigation' })}
className="ml-2 shrink-0"
className="ms-2 shrink-0"
/>
<span
aria-hidden
className="i-custom-vender-solid-general-arrow-down-round-fill size-4 shrink-0 -rotate-90 text-text-tertiary"
/>
</AgentSelectorMenuTrigger>
<AgentSelectorMenuContent
</PopoverTrigger>
<PopoverContent
side="right"
align="start"
onSelect={onSelect}
onStartFromScratch={onStartFromScratch}
/>
</AgentSelectorMenu>
className="w-100 max-w-[calc(100vw-1rem)] overflow-hidden"
>
<PopoverTitle className="sr-only">
{tAgentRoster(($) => $['roster.nodeSelector.dialogLabel'])}
</PopoverTitle>
<WorkflowAgentSelectorContent
onSelect={onSelect}
onSelectExternal={resourceKind === 'app' ? onSelectExternal : undefined}
onStartFromScratch={onStartFromScratch}
onRequestClose={() => setOpen(false)}
/>
</PopoverContent>
</Popover>
)
}
@@ -11,6 +11,7 @@ import { Fragment, memo, useCallback, useId, useMemo, useState } from 'react'
import { useTranslation } from 'react-i18next'
import { useStoreApi } from 'reactflow'
import Badge from '@/app/components/base/badge'
import { createExternalAgentWorkflowPreviewData } from '@/features/agent-v2/external-agent/workflow-preview/preview'
import BlockIcon from '../block-icon'
import { useHumanInputMigration } from '../nodes/human-input-v2/migration/context'
import { getHumanInputCreationPolicy } from '../nodes/human-input-v2/migration/policy'
@@ -142,6 +143,9 @@ const Blocks = ({
version: '2',
})
}
onSelectExternal={(agent) =>
onSelect(BlockEnum.AgentV2, createExternalAgentWorkflowPreviewData(agent))
}
/>
)
}
@@ -9,6 +9,7 @@ import type {
} from '../../plugins/types'
import type { Collection, CollectionProviderType, Event } from '../../tools/types'
import type { TypeWithI18N } from '@/app/components/header/account-setting/model-provider-page/declarations'
import type { ExternalAgentWorkflowPreviewDefaultValue } from '@/features/agent-v2/external-agent/workflow-preview/types'
export const TabType = {
Start: 'start',
@@ -123,7 +124,10 @@ type AgentDefaultValue = {
export type PluginDefaultValue = ToolDefaultValue | DataSourceDefaultValue | TriggerDefaultValue
export type BlockDefaultValue = PluginDefaultValue | AgentDefaultValue
export type BlockDefaultValue =
| PluginDefaultValue
| AgentDefaultValue
| ExternalAgentWorkflowPreviewDefaultValue
export type ToolValue = {
provider_name: string
@@ -0,0 +1,26 @@
import type { ExternalAgentWorkflowPreviewAgent } from '@/features/agent-v2/external-agent/workflow-preview/types'
// Local examples for the external Agent picker until its API is available.
export const externalWorkflowAgentExamples: readonly ExternalAgentWorkflowPreviewAgent[] = [
{
key: 'contract-review',
name: 'Contract Review Agent',
role: 'Legal & compliance',
description: 'Reviews contract clauses against your company’s policies.',
enabled: true,
},
{
key: 'document-review',
name: 'Document Review Agent',
role: 'Document review',
description: 'Reviews documents and highlights information that needs attention.',
enabled: true,
},
{
key: 'invoice-audit',
name: 'Invoice Audit Agent',
role: 'Finance',
description: 'Checks invoices for discrepancies and missing information.',
enabled: false,
},
]
@@ -0,0 +1,250 @@
import type { AgentInviteOptionResponse } from '@dify/contracts/api/console/agent/types.gen'
import type { AgentRosterNodeData } from './types'
import type { ExternalAgentWorkflowPreviewAgent } from '@/features/agent-v2/external-agent/workflow-preview/types'
import { Button } from '@langgenius/dify-ui/button'
import { Tabs, TabsList, TabsPanel, TabsTab } from '@langgenius/dify-ui/tabs'
import { keepPreviousData, useQuery } from '@tanstack/react-query'
import { useDebounce } from 'ahooks'
import { useAtomValue } from 'jotai'
import { useState } from 'react'
import { useTranslation } from 'react-i18next'
import AppIcon from '@/app/components/base/app-icon'
import { SearchInput } from '@/app/components/base/search-input'
import { editorAppIdAtom } from '@/app/components/workflow/editor-resource/state'
import Link from '@/next/link'
import { consoleQuery } from '@/service/console'
import { externalWorkflowAgentExamples } from './workflow-agent-selector-preview'
type AgentSource = 'all' | 'dify' | 'external'
type WorkflowAgentSelectorContentProps = {
onSelect: (agent: AgentRosterNodeData) => void
onSelectExternal?: (agent: ExternalAgentWorkflowPreviewAgent) => void
onStartFromScratch: () => void
onRequestClose: () => void
}
export function WorkflowAgentSelectorContent({
onSelect,
onSelectExternal,
onStartFromScratch,
onRequestClose,
}: WorkflowAgentSelectorContentProps) {
const { t } = useTranslation(['agentRoster', 'workflow', 'common'])
const appId = useAtomValue(editorAppIdAtom)
const [source, setSource] = useState<AgentSource>('all')
const [search, setSearch] = useState('')
const keyword = useDebounce(search.trim(), { wait: 300 })
const agentsQuery = useQuery({
...consoleQuery.agent.inviteOptions.get.queryOptions({
input: {
query: {
limit: 8,
page: 1,
...(appId ? { app_id: appId } : {}),
...(keyword ? { keyword } : {}),
},
},
}),
enabled: source !== 'external',
staleTime: 0,
placeholderData: keepPreviousData,
})
const difyAgents = source === 'external' ? [] : (agentsQuery.data?.data ?? [])
const externalAgents =
source === 'dify' || !onSelectExternal
? []
: externalWorkflowAgentExamples.filter((agent) =>
`${agent.name} ${agent.role} ${agent.description}`
.toLocaleLowerCase()
.includes(search.trim().toLocaleLowerCase()),
)
const isLoading = source !== 'external' && agentsQuery.isPending
const hasDifyError = source !== 'external' && agentsQuery.isError
const isEmpty =
!isLoading && !hasDifyError && difyAgents.length === 0 && externalAgents.length === 0
const sources: AgentSource[] = onSelectExternal ? ['all', 'dify', 'external'] : ['all', 'dify']
const selectDifyAgent = (agent: AgentInviteOptionResponse) => {
if (!agent.active_config_snapshot_id) return
onRequestClose()
onSelect({
description: agent.description,
icon: agent.icon,
icon_background: agent.icon_background,
icon_type: agent.icon_type,
id: agent.id,
name: agent.name,
role: agent.role,
})
}
return (
<>
<div className="p-2">
<SearchInput
value={search}
onValueChange={setSearch}
aria-label={t(($) => $['roster.searchLabel'])}
placeholder={t(($) => $['roster.nodeSelector.searchAgentsPlaceholder'])}
/>
</div>
<Tabs
value={source}
onValueChange={(value) => {
if (value === 'all' || value === 'dify' || value === 'external') setSource(value)
}}
>
<TabsList
aria-label={t(($) => $['roster.nodeSelector.dialogLabel'])}
className="h-8 gap-0.5 border-b border-divider-subtle px-3"
>
{sources.map((tab) => (
<TabsTab
key={tab}
value={tab}
className="h-6 rounded-md border-0 px-2 py-1 system-xs-medium text-text-tertiary data-active:bg-state-base-hover-alt data-active:text-text-primary"
>
{t(($) => $[`roster.nodeSelector.tabs.${tab}`])}
</TabsTab>
))}
</TabsList>
{sources.map((tab) => (
<TabsPanel
key={tab}
value={tab}
className="flex max-h-64 min-h-26 flex-col overflow-y-auto overscroll-contain p-1"
>
{source === tab && (
<>
{source !== 'external' && !search.trim() && (
<Button
variant="ghost"
className="h-8 w-full shrink-0 justify-start gap-2 rounded-lg ps-3 pe-2 system-sm-medium"
onClick={() => {
onRequestClose()
onStartFromScratch()
}}
>
<span
aria-hidden
className="i-ri-add-line size-4 shrink-0 text-text-tertiary"
/>
{t(($) => $['roster.nodeSelector.startFromScratch'])}
</Button>
)}
{isLoading && <DifyAgentSelectorSkeleton />}
{hasDifyError && (
<p role="status" className="px-3 py-2 system-sm-regular text-text-tertiary">
{t(($) => $['roster.loadingError'])}
</p>
)}
{!hasDifyError &&
difyAgents.map((agent) => (
<Button
key={agent.id}
variant="ghost"
disabled={!agent.active_config_snapshot_id}
focusableWhenDisabled
className="h-auto min-h-8 w-full shrink-0 justify-start gap-2 rounded-lg py-1 ps-3 pe-2"
onClick={() => selectDifyAgent(agent)}
>
<AppIcon size="tiny" icon={{ ...agent, icon_url: agent.icon }} />
<span className="flex min-w-0 flex-1 flex-col text-start">
<span className="flex min-w-0 items-center gap-2">
<span className="truncate system-sm-medium text-text-secondary">
{agent.name}
</span>
{source === 'all' && (
<span className="shrink-0 system-xs-regular text-text-quaternary">
{t(($) => $['roster.nodeSelector.source.dify'])}
</span>
)}
</span>
{(agent.role || agent.description) && (
<span className="truncate system-xs-regular text-text-tertiary">
{agent.role || agent.description}
</span>
)}
{!agent.active_config_snapshot_id && (
<span className="system-xs-regular text-text-tertiary">
{t(($) => $['nodes.agent.modelNotSelected'], { ns: 'workflow' })}
</span>
)}
</span>
</Button>
))}
{externalAgents.map((agent) => (
<Button
key={agent.key}
variant="ghost"
disabled={!agent.enabled}
className="h-8 w-full shrink-0 justify-start gap-2 rounded-lg ps-3 pe-2 disabled:opacity-50"
onClick={() => {
onRequestClose()
onSelectExternal?.(agent)
}}
>
<AppIcon
size="tiny"
icon={agent.icon}
className="rounded-[7px] border-0"
fallback={{
content: (
<span
aria-hidden
className="i-ri-robot-2-line size-3.5 text-text-accent"
/>
),
background: 'var(--color-components-icon-bg-indigo-soft)',
}}
/>
<span className="truncate system-sm-medium text-text-secondary">
{agent.name}
</span>
<span className="shrink-0 system-xs-regular text-text-quaternary">
{agent.enabled
? t(($) => $['roster.nodeSelector.source.external'])
: t(($) => $['external.filters.disabled'])}
</span>
</Button>
))}
{isEmpty && (
<p role="status" className="px-3 py-2 system-sm-regular text-text-tertiary">
{search.trim()
? t(($) => $['roster.emptySearch'])
: t(($) => $['roster.empty'])}
</p>
)}
</>
)}
</TabsPanel>
))}
</Tabs>
<div className="flex h-8 items-center border-t border-divider-subtle px-4">
<Link
href="/agents"
target="_blank"
rel="noopener noreferrer"
className="inline-flex items-center gap-0.5 rounded-sm system-xs-medium text-text-tertiary hover:text-text-secondary focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-state-accent-solid"
>
{t(($) => $['roster.nodeSelector.manageInAgentConsole'])}
<span aria-hidden className="i-ri-arrow-right-up-line size-3" />
</Link>
</div>
</>
)
}
function DifyAgentSelectorSkeleton() {
return (
<div aria-hidden className="space-y-1 p-2">
{[0, 1, 2].map((item) => (
<div key={item} className="flex h-8 items-center gap-2 opacity-20">
<span className="size-6 rounded-md bg-text-quaternary" />
<span className="h-2 w-32 rounded-xs bg-text-quaternary" />
</div>
))}
</div>
)
}
@@ -17,6 +17,8 @@ import ReactFlow, { ReactFlowProvider, useEdges, useNodes } from 'reactflow'
import { CollectionType } from '@/app/components/tools/types'
import { toast } from '@/app/notifications'
import { AppToaster } from '@/app/notifications/app-toaster'
import { createExternalAgentWorkflowPreviewData } from '@/features/agent-v2/external-agent/workflow-preview/preview'
import { createExternalRosterPreview } from '@/features/agent-v2/roster/external-roster-preview'
import { consoleQuery } from '@/service/console'
import { seedModelProviderQueries } from '@/test/console/model-provider'
import { createConsoleQueryWrapper } from '@/test/console/query-data'
@@ -687,6 +689,49 @@ describe('useChecklistBeforePublish', () => {
})
describe('useWorkflowRunValidation', () => {
it('blocks running and publishing a graph containing a local external agent preview', async () => {
const graph = buildConnectedGraph()
const preview = createNode({
id: 'external-preview',
data: {
type: BlockEnum.Agent,
title: 'External Agent',
...createExternalAgentWorkflowPreviewData(createExternalRosterPreview()[1]!),
},
})
const { wrapper } = createChecklistWrapper({
nodes: [...graph.nodes, preview],
edges: [
createEdge({ source: 'start', target: preview.id }),
createEdge({ source: preview.id, target: 'code' }),
createEdge({ source: 'code', target: 'end' }),
],
})
const { result } = renderHook(
() => ({
nodes: useNodes<CommonNodeType>(),
run: useWorkflowRunValidation(),
publish: useChecklistBeforePublish(),
}),
{ wrapper },
)
expect(result.current.run.warningNodes).toContainEqual(
expect.objectContaining({
id: preview.id,
errorMessages: ['agentRoster.external.workflow.runUnavailable'],
canNavigate: true,
}),
)
act(() => {
expect(result.current.run.validateBeforeRun()).toBe(false)
})
await act(async () => {
expect(await result.current.publish.handleCheckBeforePublish()).toBe(false)
})
expect(result.current.nodes.find((node) => node.id === preview.id)?.data.selected).toBe(true)
})
it('blocks a run with disconnected nodes and explains why', async () => {
const { nodes } = buildConnectedGraph()
const { wrapper } = createChecklistWrapper({ nodes, edges: [] })
@@ -1,6 +1,9 @@
import { act, waitFor } from '@testing-library/react'
import { createExternalAgentWorkflowPreviewData } from '@/features/agent-v2/external-agent/workflow-preview/preview'
import { createExternalRosterPreview } from '@/features/agent-v2/roster/external-roster-preview'
import { createEdge, createNode } from '../../__tests__/fixtures'
import { renderWorkflowFlowHook } from '../../__tests__/workflow-test-env'
import { BlockEnum } from '../../types'
import { useCollaborativeWorkflow } from '../use-collaborative-workflow'
const mocks = vi.hoisted(() => ({
@@ -76,3 +79,47 @@ it('updates the local graph while stripping selection state from broadcasts', as
expect(previousEdges[0].data).not.toHaveProperty('_connectedNodeIsSelected')
expect(nextEdges[0].data).not.toHaveProperty('_connectedNodeIsSelected')
})
it('keeps external agent previews and their edges local through edits and deletion', async () => {
mocks.canApplyLocalGraphMutation.mockReturnValue(true)
const { result } = setup()
await waitFor(() => expect(result.current.getState().nodes).toHaveLength(2))
const originalNodes = result.current.getState().nodes
const preview = createNode({
id: 'external-preview',
data: {
type: BlockEnum.Agent,
...createExternalAgentWorkflowPreviewData(createExternalRosterPreview()[1]!),
},
})
const previewEdge = createEdge({ id: 'preview-edge', source: oldNode.id, target: preview.id })
act(() => {
result.current.setNodes([...originalNodes, preview], false)
result.current.setEdges([oldEdge, previewEdge], false)
})
expect(mocks.setNodes).not.toHaveBeenCalled()
expect(mocks.setEdges).not.toHaveBeenCalled()
act(() => {
result.current.setNodes([newNode, originalNodes[1]!, preview])
result.current.setEdges([newEdge, previewEdge])
})
expect(result.current.getState().nodes).toHaveLength(3)
expect(result.current.getState().edges).toHaveLength(2)
const [oldBroadcastNodes, newBroadcastNodes] = mocks.setNodes.mock.lastCall!
expect(oldBroadcastNodes.map((node: { id: string }) => node.id)).toEqual(['old-node', 'target'])
expect(newBroadcastNodes.map((node: { id: string }) => node.id)).toEqual(['new-node', 'target'])
expect(mocks.setEdges).toHaveBeenLastCalledWith(
[expect.objectContaining({ id: oldEdge.id })],
[expect.objectContaining({ id: newEdge.id })],
)
act(() => {
result.current.setNodes([newNode, originalNodes[1]!])
result.current.setEdges([newEdge])
})
expect(mocks.setEdges).toHaveBeenLastCalledWith(
[expect.objectContaining({ id: newEdge.id })],
[expect.objectContaining({ id: newEdge.id })],
)
})
@@ -6,9 +6,13 @@ import { zWorkflowAgentComposerResponse } from '@dify/contracts/api/console/apps
import { act, waitFor } from '@testing-library/react'
import { use, useLayoutEffect } from 'react'
import { useEdges, useNodes } from 'reactflow'
import { createExternalAgentWorkflowPreviewData } from '@/features/agent-v2/external-agent/workflow-preview/preview'
import { isExternalAgentWorkflowPreviewData } from '@/features/agent-v2/external-agent/workflow-preview/types'
import { createExternalRosterPreview } from '@/features/agent-v2/roster/external-roster-preview'
import { seedModelProviderQueries } from '@/test/console/model-provider'
import { createConsoleQueryWrapper, seedAppDslVersion } from '@/test/console/query-data'
import { createDeferred } from '@/test/deferred'
import { guardUnexpectedFetch } from '@/test/unexpected-fetch'
import { createEdge, createNode } from '../../__tests__/fixtures'
import { renderWorkflowFlowHook } from '../../__tests__/workflow-test-env'
import { WORKFLOW_COMMON_NODES } from '../../constants/node'
@@ -543,6 +547,56 @@ it.each(['add', 'replace'] as const)(
},
)
it('adds an external agent preview beside an existing connection without changing or syncing the workflow graph', async ({
onTestFinished,
}) => {
onTestFinished(guardUnexpectedFetch())
const originalNodes = [codeNode('a'), codeNode('b')]
const originalEdge = createEdge({ id: 'original-connection', source: 'a', target: 'b' })
const { result, sync } = setup({ nodes: originalNodes, edges: [originalEdge] })
const originalConfiguration = structuredClone(
result.current.nodes.map((node) => ({
id: node.id,
position: node.position,
data: { ...node.data, selected: false },
})),
)
vi.useFakeTimers({ toFake: ['setTimeout', 'clearTimeout', 'Date'] })
onTestFinished(() => {
vi.useRealTimers()
})
act(() =>
result.current.handleNodeAdd(
{
nodeType: BlockEnum.AgentV2,
pluginDefaultValue: createExternalAgentWorkflowPreviewData(
createExternalRosterPreview()[1]!,
),
},
{ prevNodeId: 'a', nextNodeId: 'b' },
),
)
await act(async () => vi.advanceTimersByTimeAsync(2000))
const previews = result.current.nodes.filter((node) =>
isExternalAgentWorkflowPreviewData(node.data),
)
expect(previews).toHaveLength(1)
expect(result.current.edges).toEqual([originalEdge])
expect(
result.current.nodes
.filter((node) => !isExternalAgentWorkflowPreviewData(node.data))
.map((node) => ({
id: node.id,
position: node.position,
data: { ...node.data, selected: false },
})),
).toEqual(originalConfiguration)
expect(sync).not.toHaveBeenCalled()
expect(createAgent).not.toHaveBeenCalled()
})
it('creates an inline Agent through the service boundary and resolves the pending graph node', async () => {
const request = createDeferred<Awaited<ReturnType<typeof createAgent>>>()
createAgent.mockReturnValue(request.promise)
@@ -43,6 +43,7 @@ import {
getAgentToolPublishIssues,
useAgentToolPresentation,
} from '@/features/agent-v2/agent-detail/configure/tool-provider-catalog'
import { isExternalAgentWorkflowPreviewData } from '@/features/agent-v2/external-agent/workflow-preview/types'
import { consoleQuery } from '@/service/console'
import { fetchDatasets } from '@/service/datasets'
import { useAllTriggerPlugins } from '@/service/use-triggers'
@@ -181,7 +182,7 @@ const getDuplicateEndOutputMessages = (
}
export const useChecklist = (nodes: Node[], edges: Edge[]) => {
const { t } = useTranslation(['agentV2', 'common', 'workflow', 'modelProvider'])
const { t } = useTranslation(['agentV2', 'common', 'workflow', 'modelProvider', 'agentRoster'])
const language = useGetLanguage()
const workspaceId = useAtomValue(currentWorkspaceIdAtom)
const contactIds = useMemo(() => getHumanInputContactIds(nodes), [nodes])
@@ -410,6 +411,16 @@ export const useChecklist = (nodes: Node[], edges: Edge[]) => {
for (let i = 0; i < filteredNodes.length; i++) {
const node = filteredNodes[i]
if (node && isExternalAgentWorkflowPreviewData(node.data)) {
list.push({
id: node.id,
type: BlockEnum.AgentV2,
title: node.data.title,
errorMessages: [t(($) => $['external.workflow.runUnavailable'], { ns: 'agentRoster' })],
canNavigate: true,
})
continue
}
let moreDataForCheckValid: CheckValidExtraData
let usedVars: ValueSelector[] = []
@@ -756,6 +767,19 @@ export const useChecklistBeforePublish = () => {
const { dataSourceList, environmentVariables = [] } = workflowStore.getState()
const nodes = getNodes()
const filteredNodes = nodes.filter((node) => node.type === CUSTOM_NODE)
const externalAgentPreview = filteredNodes.find((node) =>
isExternalAgentWorkflowPreviewData(node.data),
)
if (externalAgentPreview) {
store.getState().setNodes(
nodes.map((node) => ({
...node,
selected: node.id === externalAgentPreview.id,
data: { ...node.data, selected: node.id === externalAgentPreview.id },
})),
)
return false
}
const duplicateEndOutputMessages = getDuplicateEndOutputMessages(filteredNodes, t)
const { validNodes, maxDepth } = getValidTreeNodes(filteredNodes, edges)
@@ -1,6 +1,8 @@
import type { Edge, Node } from '../types'
import { useRefWithInit } from '@langgenius/dify-ui/use-ref-with-init'
import { useCallback, useMemo } from 'react'
import { useStoreApi } from 'reactflow'
import { isExternalAgentWorkflowPreviewData } from '@/features/agent-v2/external-agent/workflow-preview/types'
import { collaborationManager } from '../collaboration/core/collaboration-manager'
const sanitizeNodeForBroadcast = (node: Node): Node => {
@@ -33,6 +35,7 @@ const sanitizeEdgeForBroadcast = (edge: Edge): Edge => {
export const useCollaborativeWorkflow = () => {
const store = useStoreApi()
const previewNodeIds = useRefWithInit(() => new Set<string>()).current
const { setNodes: collabSetNodes, setEdges: collabSetEdges } = collaborationManager
const setNodes = useCallback(
@@ -42,33 +45,48 @@ export const useCollaborativeWorkflow = () => {
source = 'use-collaborative-workflow:setNodes',
) => {
const { getNodes, setNodes: reactFlowSetNodes } = store.getState()
const oldNodes = getNodes()
for (const node of [...oldNodes, ...newNodes]) {
if (isExternalAgentWorkflowPreviewData(node.data)) previewNodeIds.add(node.id)
}
if (shouldBroadcast) {
if (!collaborationManager.canApplyLocalGraphMutation()) return
const oldNodes = getNodes()
collabSetNodes(
oldNodes.map(sanitizeNodeForBroadcast),
newNodes.map(sanitizeNodeForBroadcast),
oldNodes
.filter((node) => !isExternalAgentWorkflowPreviewData(node.data))
.map(sanitizeNodeForBroadcast),
newNodes
.filter((node) => !isExternalAgentWorkflowPreviewData(node.data))
.map(sanitizeNodeForBroadcast),
source,
)
}
reactFlowSetNodes(newNodes)
},
[store, collabSetNodes],
[store, collabSetNodes, previewNodeIds],
)
const setEdges = useCallback(
(newEdges: Edge[], shouldBroadcast: boolean = true) => {
const { edges, setEdges: reactFlowSetEdges } = store.getState()
const { edges, getNodes, setEdges: reactFlowSetEdges } = store.getState()
for (const node of getNodes()) {
if (isExternalAgentWorkflowPreviewData(node.data)) previewNodeIds.add(node.id)
}
if (shouldBroadcast) {
if (!collaborationManager.canApplyLocalGraphMutation()) return
collabSetEdges(edges.map(sanitizeEdgeForBroadcast), newEdges.map(sanitizeEdgeForBroadcast))
const canBroadcastEdge = (edge: Edge) =>
!previewNodeIds.has(edge.source) && !previewNodeIds.has(edge.target)
collabSetEdges(
edges.filter(canBroadcastEdge).map(sanitizeEdgeForBroadcast),
newEdges.filter(canBroadcastEdge).map(sanitizeEdgeForBroadcast),
)
}
reactFlowSetEdges(newEdges)
},
[store, collabSetEdges],
[store, collabSetEdges, previewNodeIds],
)
const collaborativeStore = useCallback(() => {
@@ -21,6 +21,7 @@ import { useTranslation } from 'react-i18next'
import { getConnectedEdges, getOutgoers, useReactFlow } from 'reactflow'
import { isChatflowAtom } from '@/app/components/workflow/editor-resource/state'
import { toast } from '@/app/notifications'
import { isExternalAgentWorkflowPreviewData } from '@/features/agent-v2/external-agent/workflow-preview/types'
import { consoleQuery } from '@/service/console'
import { collaborationManager } from '../collaboration/core/collaboration-manager'
import {
@@ -884,6 +885,27 @@ export const useNodesInteractions = () => {
y: 0,
},
})
if (isExternalAgentWorkflowPreviewData(newNode.data)) {
const anchor = nodes.find((node) => node.id === (nextNodeId ?? prevNodeId))
const anchorPosition = anchor?.positionAbsolute ?? anchor?.position
const mousePosition = workflowStore.getState().mousePosition
newNode.position = anchorPosition
? { x: anchorPosition.x + NODE_WIDTH_X_OFFSET, y: anchorPosition.y + Y_OFFSET }
: reactflow.screenToFlowPosition({ x: mousePosition.pageX, y: mousePosition.pageY })
setNodes(
[
...nodes.map((node) => ({
...node,
selected: false,
data: { ...node.data, selected: false },
})),
newNode,
],
false,
)
saveStateToHistory(WorkflowHistoryEvent.NodeAdd, { nodeId: newNode.id })
return
}
if (prevNodeId && !nextNodeId) {
const prevNodeIndex = nodes.findIndex((node) => node.id === prevNodeId)
const prevNode = nodes[prevNodeIndex]
@@ -1356,6 +1378,7 @@ export const useNodesInteractions = () => {
createInlineAgentBindingForNode,
saveStateToHistory,
workflowStore,
reactflow,
getAfterNodesInSameBranch,
nodesMetaDataMap,
t,
@@ -1375,6 +1398,15 @@ export const useNodesInteractions = () => {
const nodeMetaData = nodesMetaDataMap?.[nodeType]
if (!nodeMetaData) return
const { defaultValue } = nodeMetaData
if (
isExternalAgentWorkflowPreviewData({
...defaultValue,
...pluginDefaultValue,
} as Node['data'])
) {
handleNodeAdd({ nodeType, sourceHandle, pluginDefaultValue }, { prevNodeId: currentNodeId })
return
}
const insertionBlocker = getHumanInputInsertionBlocker(nodes, nodeType, {
...(defaultValue as Node['data']),
...pluginDefaultValue,
@@ -1576,6 +1608,7 @@ export const useNodesInteractions = () => {
saveStateToHistory,
nodesMetaDataMap,
autoGenerateWebhookUrl,
handleNodeAdd,
t,
],
)
@@ -8,6 +8,7 @@ import { useHooksStore } from '@/app/components/workflow/hooks-store'
import { useWorkflowStore } from '@/app/components/workflow/store'
import { BlockEnum, NodeRunningStatus } from '@/app/components/workflow/types'
import { canRunBySingle } from '@/app/components/workflow/utils'
import { isExternalAgentWorkflowPreviewData } from '@/features/agent-v2/external-agent/workflow-preview/types'
import { consoleQuery } from '@/service/console'
import { matchesProviderReference } from '@/utils/provider-reference'
import { useNodesInteractions } from '../hooks/use-nodes-interactions'
@@ -41,7 +42,11 @@ export function useNodeActionsMenuModel({
)
const isChildNode = !!(data.isInIteration || data.isInLoop)
const canRun = canRunWorkflow && !nodesReadOnly && canRunBySingle(data.type, isChildNode)
const canRun =
canRunWorkflow &&
!nodesReadOnly &&
!isExternalAgentWorkflowPreviewData(data) &&
canRunBySingle(data.type, isChildNode)
const isSingleRunning = data._singleRunningStatus === NodeRunningStatus.Running
const canChangeBlock = !nodeMetaData.isTypeFixed && !nodeMetaData.isUndeletable && !nodesReadOnly
const sourceHandle = useMemo(() => {
@@ -62,6 +67,7 @@ export function useNodeActionsMenuModel({
}, [data.provider_id, data.provider_type, data.type, workflowTools])
const handleRun = useCallback(() => {
if (!canRun) return
const store = workflowStore.getState()
store.setInitShowLastRunTab(true)
store.setPendingSingleRun({
@@ -70,7 +76,7 @@ export function useNodeActionsMenuModel({
})
handleNodeSelect(id)
onClose()
}, [handleNodeSelect, id, isSingleRunning, onClose, workflowStore])
}, [canRun, handleNodeSelect, id, isSingleRunning, onClose, workflowStore])
const handleCopy = useCallback(() => {
onClose()
@@ -2,9 +2,26 @@ import { RiMindMap } from '@remixicon/react'
import { useTranslation } from 'react-i18next'
import { useDocLink } from '@/context/i18n'
const FailBranchCard = () => {
const FailBranchCard = ({
learnMorePlacement = 'inline',
}: {
learnMorePlacement?: 'inline' | 'below'
}) => {
const { t } = useTranslation(['workflow'])
const docLink = useDocLink()
const learnMore = (
<a
href={docLink('/use-dify/debug/error-type')}
target="_blank"
rel="noopener noreferrer"
className="inline-flex items-center gap-1 rounded-sm text-text-accent focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-components-input-border-active"
>
{t(($) => $['common.learnMore'], { ns: 'workflow' })}
{learnMorePlacement === 'below' && (
<span aria-hidden className="i-ri-arrow-right-up-line size-3" />
)}
</a>
)
return (
<div className="px-4 pt-2">
@@ -17,17 +34,10 @@ const FailBranchCard = () => {
</div>
<div className="system-xs-regular text-text-tertiary">
{t(($) => $['nodes.common.errorHandle.failBranch.customizeTip'], { ns: 'workflow' })}
&nbsp;
<a
href={docLink('/use-dify/debug/error-type')}
target="_blank"
rel="noopener noreferrer"
className="text-text-accent"
>
{t(($) => $['common.learnMore'], { ns: 'workflow' })}
</a>
{learnMorePlacement === 'inline' && <> {learnMore}</>}
</div>
</div>
{learnMorePlacement === 'below' && <div className="mt-2 system-xs-medium">{learnMore}</div>}
</div>
)
}
@@ -7,6 +7,7 @@ import { useTranslation } from 'react-i18next'
import { useHooksStore } from '@/app/components/workflow/hooks-store'
import { NodeActionsMenu } from '@/app/components/workflow/node-actions-menu'
import { useWorkflowStore } from '@/app/components/workflow/store'
import { isExternalAgentWorkflowPreviewData } from '@/features/agent-v2/external-agent/workflow-preview/types'
import { useNodesInteractions } from '../../../hooks/use-nodes-interactions'
import { useNodesReadOnly } from '../../../hooks/use-workflow'
import { NodeRunningStatus } from '../../../types'
@@ -27,7 +28,7 @@ const NodeControl: FC<NodeControlProps> = ({ id, data, pluginInstallLocked }) =>
return (
<div
className={cn(
'pointer-events-none absolute -top-8 right-0 flex h-8 origin-bottom-right pb-1 opacity-0',
'pointer-events-none absolute inset-e-0 -top-8 flex h-8 origin-bottom-right pb-1 opacity-0',
'focus-within:pointer-events-auto focus-within:opacity-100',
!pluginInstallLocked && 'group-hover:pointer-events-auto group-hover:opacity-100',
data.selected && 'pointer-events-auto opacity-100',
@@ -40,39 +41,42 @@ const NodeControl: FC<NodeControlProps> = ({ id, data, pluginInstallLocked }) =>
onMouseDown={(e) => e.stopPropagation()}
onClick={(e) => e.stopPropagation()}
>
{canRun && !nodesReadOnly && canRunBySingle(data.type, isChildNode) && (
<button
type="button"
aria-label={
isSingleRunning
? t(($) => $['debug.variableInspect.trigger.stop'], { ns: 'workflowDebug' })
: t(($) => $['panel.runThisStep'], { ns: 'workflow' })
}
className={`flex size-6 items-center justify-center rounded-md ${isSingleRunning && 'cursor-pointer hover:bg-state-base-hover'}`}
onClick={() => {
const action = isSingleRunning ? 'stop' : 'run'
{canRun &&
!nodesReadOnly &&
!isExternalAgentWorkflowPreviewData(data) &&
canRunBySingle(data.type, isChildNode) && (
<button
type="button"
aria-label={
isSingleRunning
? t(($) => $['debug.variableInspect.trigger.stop'], { ns: 'workflowDebug' })
: t(($) => $['panel.runThisStep'], { ns: 'workflow' })
}
className={`flex size-6 items-center justify-center rounded-md ${isSingleRunning && 'cursor-pointer hover:bg-state-base-hover'}`}
onClick={() => {
const action = isSingleRunning ? 'stop' : 'run'
const store = workflowStore.getState()
store.setInitShowLastRunTab(true)
store.setPendingSingleRun({
nodeId: id,
action,
})
handleNodeSelect(id)
}}
>
{isSingleRunning ? (
<span aria-hidden className="i-custom-vender-line-mediaanddevices-stop size-3" />
) : (
<Tooltip>
<TooltipTrigger render={<span className="i-ri-play-large-line size-3" />} />
<TooltipContent>
{t(($) => $['panel.runThisStep'], { ns: 'workflow' })}
</TooltipContent>
</Tooltip>
)}
</button>
)}
const store = workflowStore.getState()
store.setInitShowLastRunTab(true)
store.setPendingSingleRun({
nodeId: id,
action,
})
handleNodeSelect(id)
}}
>
{isSingleRunning ? (
<span aria-hidden className="i-custom-vender-line-mediaanddevices-stop size-3" />
) : (
<Tooltip>
<TooltipTrigger render={<span className="i-ri-play-large-line size-3" />} />
<TooltipContent>
{t(($) => $['panel.runThisStep'], { ns: 'workflow' })}
</TooltipContent>
</Tooltip>
)}
</button>
)}
<NodeActionsMenu id={id} data={data} />
</div>
</div>
@@ -25,7 +25,6 @@ import {
getOutputDisplayType,
getOutputTypeOptionValue,
insertOutputChildAtPath,
isDefaultOutput,
toDeclaredOutputChild,
updateOutputChildAtPath,
} from './utils'
@@ -44,10 +43,10 @@ function OutputRow({
onEdit: () => void
}) {
const { t } = useTranslation(['workflow', 'common', 'workflowAgent'])
const description = getOutputDescription(output, t)
const description = editable ? output.description || '' : getOutputDescription(output, t)
return (
<div className="group flex min-h-12 flex-col rounded-lg py-0.5 focus-within:bg-state-base-hover hover:bg-state-base-hover">
<div className="flex h-6 items-center gap-x-1 pr-0.5 pl-1">
<div className="flex h-6 items-center gap-x-1 ps-1 pe-0.5">
<div className="flex min-w-0 grow items-center gap-x-1">
<span className="flex h-5 min-w-0 items-center px-1">
<span className="truncate code-sm-semibold leading-4 text-text-primary">
@@ -144,6 +143,8 @@ export function AgentOutputVariables({
onChange,
collapsed,
onCollapse,
compact = false,
reservedOutputNames = AGENT_V2_RESERVED_OUTPUT_NAMES,
}: AgentOutputVariablesProps) {
const { t } = useTranslation(['workflowAgent'])
const [editingState, setEditingState] = useState<EditingState | null>(null)
@@ -277,11 +278,28 @@ export function AgentOutputVariables({
)
}
return (
<OutputVars collapsed={isCollapsed} onCollapse={handleCollapseChange}>
<OutputVars
collapsed={isCollapsed}
onCollapse={handleCollapseChange}
operations={
compact ? (
<IconButton
aria-label={t(($) => $['nodes.agent.outputVars.newOutput'], { ns: 'workflowAgent' })}
className="me-4"
onClick={() => {
handleCollapseChange(false)
handleNewOutput()
}}
>
<span aria-hidden className="i-ri-add-line size-4" />
</IconButton>
) : undefined
}
>
<div className="pb-2">
<div className="flex flex-col">
{outputs.map((output, index) => {
const editable = !isDefaultOutput(output)
const editable = !reservedOutputNames.has(output.name)
const children = getOutputChildren(output)
const isEditingOutput =
editingState?.outputIndex === index &&
@@ -297,7 +315,7 @@ export function AgentOutputVariables({
key={`${output.name}-editing`}
editingIndex={index}
existingOutputs={outputs}
reservedNames={AGENT_V2_RESERVED_OUTPUT_NAMES}
reservedNames={reservedOutputNames}
state={editingState}
onCancel={() => setEditingState(null)}
onConfirm={handleConfirm}
@@ -332,18 +350,20 @@ export function AgentOutputVariables({
</div>
)
})}
<div className="py-1">
<Separator decorative orientation="horizontal" className="my-2 bg-divider-subtle" />
</div>
{!compact && (
<div className="py-1">
<Separator decorative orientation="horizontal" className="my-2 bg-divider-subtle" />
</div>
)}
{editingState && editingState.outputIndex == null ? (
<OutputEditCard
existingOutputs={outputs}
reservedNames={AGENT_V2_RESERVED_OUTPUT_NAMES}
reservedNames={reservedOutputNames}
state={editingState}
onCancel={() => setEditingState(null)}
onConfirm={handleConfirm}
/>
) : (
) : !compact ? (
<div className="pt-1">
<Button
size="small"
@@ -355,7 +375,7 @@ export function AgentOutputVariables({
{t(($) => $['nodes.agent.outputVars.newOutput'], { ns: 'workflowAgent' })}
</Button>
</div>
)}
) : null}
</div>
</div>
</OutputVars>
@@ -45,6 +45,8 @@ export type AgentOutputVariablesProps = {
onChange: (outputs: DeclaredOutputConfig[]) => void
collapsed?: boolean
onCollapse?: (collapsed: boolean) => void
compact?: boolean
reservedOutputNames?: ReadonlySet<string>
}
export const OUTPUT_NAME_PATTERN = /^[a-z_]\w*$/i
@@ -1,5 +1,7 @@
import type { ComponentProps, ComponentType } from 'react'
import { createElement } from 'react'
import { ExternalAgentWorkflowPreviewNode } from '@/features/agent-v2/external-agent/workflow-preview/node'
import { isExternalAgentWorkflowPreviewData } from '@/features/agent-v2/external-agent/workflow-preview/types'
import { BlockEnum } from '../types'
import { AgentV2Node } from './agent-v2/node'
import { isAgentV2NodeData } from './agent-v2/types'
@@ -36,6 +38,9 @@ type WorkflowAgentNodeProps = ComponentProps<typeof AgentNode> | ComponentProps<
type WorkflowComponentMap = Record<string, ComponentType<Record<string, never>>>
function WorkflowAgentNode(props: WorkflowAgentNodeProps) {
if (isExternalAgentWorkflowPreviewData(props.data))
return createElement(ExternalAgentWorkflowPreviewNode, { data: props.data })
if (isAgentV2NodeData(props.data))
return createElement(AgentV2Node, props as ComponentProps<typeof AgentV2Node>)
@@ -65,7 +70,7 @@ export const NodeComponentMap = {
[BlockEnum.DocExtractor]: DocExtractorNode,
[BlockEnum.ListFilter]: ListFilterNode,
[BlockEnum.Agent]: WorkflowAgentNode,
[BlockEnum.AgentV2]: AgentV2Node,
[BlockEnum.AgentV2]: WorkflowAgentNode,
[BlockEnum.DataSource]: DataSourceNode,
[BlockEnum.KnowledgeBase]: KnowledgeBaseNode,
[BlockEnum.HumanInput]: WorkflowHumanInputNode,
@@ -1,5 +1,7 @@
import type { Node } from '../types'
import { memo } from 'react'
import { ExternalAgentWorkflowPreviewPanel } from '@/features/agent-v2/external-agent/workflow-preview/panel'
import { isExternalAgentWorkflowPreviewData } from '@/features/agent-v2/external-agent/workflow-preview/types'
import { CUSTOM_NODE } from '../constants'
import { getNodeCatalogType } from '../utils/node'
import BasePanel from './_base/components/workflow-panel'
@@ -15,6 +17,9 @@ const Panel = memo((props: PanelProps) => {
const nodeData = props.data
if (nodeClass !== CUSTOM_NODE) return null
if (isExternalAgentWorkflowPreviewData(nodeData))
return <ExternalAgentWorkflowPreviewPanel id={props.id} data={nodeData} />
const PanelComponent = PanelComponentMap[nodeData.type]!
return (
@@ -3,6 +3,7 @@ import type { LoopNodeType } from '../nodes/loop/types'
import type { CommonNodeType, Node } from '../types'
import { Position } from 'reactflow'
import { CUSTOM_SIMPLE_NODE } from '@/app/components/workflow/simple-node/constants'
import { isExternalAgentWorkflowPreviewData } from '@/features/agent-v2/external-agent/workflow-preview/types'
import { CUSTOM_NODE, NESTED_ELEMENT_Z_INDEX } from '../constants'
import { isAgentV2NodeData } from '../nodes/agent-v2/types'
import { isHumanInputV2NodeData } from '../nodes/human-input-v2/types'
@@ -11,6 +12,7 @@ import { CUSTOM_LOOP_START_NODE } from '../nodes/loop-start/constants'
import { BlockEnum } from '../types'
export function getNodeCatalogType(data: CommonNodeType): BlockEnum {
if (isExternalAgentWorkflowPreviewData(data)) return BlockEnum.AgentV2
if (isAgentV2NodeData(data)) return BlockEnum.AgentV2
if (isHumanInputV2NodeData(data)) return BlockEnum.HumanInputV2
return data.type
@@ -0,0 +1,325 @@
import type { ComponentType } from 'react'
import type { NodeProps } from 'reactflow'
import type { ExternalAgentWorkflowPreviewNodeData } from '../types'
import type { CommonNodeType } from '@/app/components/workflow/types'
import { createInstance } from 'i18next'
import { I18nextProvider } from 'react-i18next'
import ReactFlow, { Handle, Position, ReactFlowProvider, useNodes } from 'reactflow'
import { page, userEvent } from 'vite-plus/test/browser'
import { render } from 'vitest-browser-react'
import { createEdge, createNode } from '@/app/components/workflow/__tests__/fixtures'
import { WorkflowContext } from '@/app/components/workflow/context'
import { EditorResourceScope } from '@/app/components/workflow/editor-resource/provider'
import { HooksStoreContext } from '@/app/components/workflow/hooks-store/provider'
import { createHooksStore } from '@/app/components/workflow/hooks-store/store'
import { NodeComponentMap } from '@/app/components/workflow/nodes/components'
import Panel from '@/app/components/workflow/nodes/panel'
import { createWorkflowStore } from '@/app/components/workflow/store/workflow'
import { BlockEnum, InputVarType, VarType } from '@/app/components/workflow/types'
import { EventEmitterContextProvider } from '@/context/event-emitter-provider'
import { createExternalRosterPreview } from '@/features/agent-v2/roster/external-roster-preview'
import agentRoster from '@/i18n/locales/en-US/agent-roster.json'
import common from '@/i18n/locales/en-US/common.json'
import workflowAgent from '@/i18n/locales/en-US/workflow-agent.json'
import workflowDebug from '@/i18n/locales/en-US/workflow-debug.json'
import workflow from '@/i18n/locales/en-US/workflow.json'
import { consoleQuery } from '@/service/console'
import { seedModelProviderQueries } from '@/test/console/model-provider'
import { createConsoleQueryWrapper } from '@/test/console/query-data'
import { seedToolProviders } from '@/test/console/tool-providers'
import { guardUnexpectedFetch } from '@/test/unexpected-fetch'
import { createExternalAgentWorkflowPreviewData } from '../preview'
import { isExternalAgentWorkflowPreviewData } from '../types'
import 'reactflow/dist/style.css'
vi.mock('react-i18next', async (importOriginal) => importOriginal())
// The Agent catalog entry routes external preview data before mounting Dify API owners.
const AgentNode = NodeComponentMap[BlockEnum.Agent] as unknown as ComponentType<{
id: string
data: ExternalAgentWorkflowPreviewNodeData
}>
function CanvasNode({ id, data }: NodeProps<CommonNodeType>) {
return (
<>
<Handle type="target" position={Position.Left} />
{isExternalAgentWorkflowPreviewData(data) ? (
<AgentNode id={id} data={data} />
) : (
<div>{data.title}</div>
)}
<Handle type="source" position={Position.Right} />
</>
)
}
const nodeTypes = { custom: CanvasNode }
function SelectedPanel() {
const node = useNodes<CommonNodeType>().find((node) => node.selected || node.data.selected)
return node ? <Panel type={node.type} id={node.id} data={node.data} /> : null
}
async function setup() {
await page.viewport(1280, 1000)
const i18n = createInstance()
await i18n.init({
lng: 'en-US',
resources: { 'en-US': { agentRoster, common, workflowAgent, workflowDebug, workflow } },
keySeparator: false,
interpolation: { escapeValue: false },
})
const { queryClient, wrapper: QueryProvider } = createConsoleQueryWrapper({
currentWorkspace: { role: 'owner' },
})
onTestFinished(() => queryClient.clear())
seedModelProviderQueries(queryClient)
seedToolProviders(queryClient, {
builtInTools: [],
customTools: [],
workflowTools: [],
mcpTools: [],
})
queryClient.setQueryData(consoleQuery.spec.schemaDefinitions.get.queryKey(), [])
const syncDraft = vi.fn(async () => {})
const hooks = createHooksStore({ doSyncWorkflowDraft: syncDraft })
const store = createWorkflowStore({})
const agent = createExternalRosterPreview()[1]!
const data: ExternalAgentWorkflowPreviewNodeData = {
type: BlockEnum.Agent,
title: 'Agent',
desc: '',
selected: true,
...createExternalAgentWorkflowPreviewData(agent),
}
const nodes = [
createNode({
id: 'input',
position: { x: 0, y: 0 },
data: {
type: BlockEnum.Start,
title: 'Input',
variables: [
{
variable: 'contract_text',
type: InputVarType.paragraph,
label: 'Contract text',
required: false,
},
],
},
}),
createNode({
id: 'policy',
position: { x: 0, y: 100 },
data: {
type: BlockEnum.Code,
title: 'Policy',
outputs: { result: { type: VarType.string } },
},
}),
createNode({ id: 'external-agent', position: { x: 160, y: 50 }, selected: true, data }),
]
const edges = [
createEdge({ source: 'input', target: 'policy' }),
createEdge({ source: 'policy', target: 'external-agent' }),
]
await render(
<QueryProvider>
<EditorResourceScope
resource={{
kind: 'app',
appId: 'external-preview-app',
mode: 'workflow',
collaborationEnabled: false,
}}
>
<WorkflowContext value={store}>
<HooksStoreContext value={hooks}>
<EventEmitterContextProvider>
<I18nextProvider i18n={i18n}>
<ReactFlowProvider>
<div className="isolate flex gap-4" style={{ height: 960 }}>
<section aria-label="Agent canvas" style={{ width: 480, height: 960 }}>
<ReactFlow
defaultNodes={nodes}
defaultEdges={edges}
nodeTypes={nodeTypes}
fitView
/>
</section>
<SelectedPanel />
</div>
</ReactFlowProvider>
</I18nextProvider>
</EventEmitterContextProvider>
</HooksStoreContext>
</WorkflowContext>
</EditorResourceScope>
</QueryProvider>,
)
onTestFinished(() => {
expect(syncDraft).not.toHaveBeenCalled()
})
const panel = page.getByRole('region', { name: 'Contract Review Agent', exact: true })
await expect.element(panel).toBeVisible()
return { panel, canvas: page.getByRole('region', { name: 'Agent canvas', exact: true }) }
}
async function closeAndReopen(
panel: ReturnType<typeof page.getByRole>,
name = 'Contract Review Agent',
) {
await panel.getByRole('button', { name: 'Close', exact: true }).click()
await expect.element(panel).not.toBeInTheDocument()
await page
.getByRole('region', { name: 'Agent canvas', exact: true })
.getByText(name, { exact: true })
.click()
const reopened = page.getByRole('region', { name, exact: true })
await expect.element(reopened).toBeVisible()
return reopened
}
async function replaceTask(task: ReturnType<typeof page.getByRole>, value: string) {
await task.click()
await userEvent.keyboard(
/Mac/.test(navigator.userAgent) ? '{Meta>}a{/Meta}' : '{Control>}a{/Control}',
)
await userEvent.keyboard('{Backspace}')
await expect.element(task).toHaveTextContent('')
await task.fill(value)
}
beforeEach(({ onTestFinished }) => {
onTestFinished(guardUnexpectedFetch())
})
it('shows the external badge and updates the canvas agent through the change picker without Dify APIs', async () => {
const { panel, canvas } = await setup()
await expect.element(panel.getByText('External', { exact: true })).toBeVisible()
await expect.element(canvas.getByText('External', { exact: true })).toBeVisible()
await expect
.element(panel.getByRole('tab', { name: 'Settings', exact: true }))
.toHaveAttribute('aria-selected', 'true')
await expect
.element(panel.getByRole('button', { name: /Run this step/i }))
.toHaveAttribute('aria-disabled', 'true')
await panel.getByRole('button', { name: 'Change', exact: true }).click()
await page.getByRole('button', { name: /Document Review Agent/ }).click()
const changedPanel = page.getByRole('region', { name: 'Document Review Agent', exact: true })
await expect.element(changedPanel).toBeVisible()
await expect.element(canvas.getByText('Document Review Agent', { exact: true })).toBeVisible()
await expect
.element(canvas.getByText('Contract Review Agent', { exact: true }))
.not.toBeInTheDocument()
await closeAndReopen(changedPanel, 'Document Review Agent')
})
it('keeps edits made in the real task editor when closing and reopening the node', async () => {
const { panel } = await setup()
const task = panel.getByRole('textbox', { name: 'Agent task', exact: true })
await expect.element(task).toMatchTextContent(/Policy/)
await expect.element(task).toMatchTextContent(/Contract/)
await replaceTask(task, 'Review the renewal terms and highlight unusual liability clauses.')
await userEvent.tab()
const reopened = await closeAndReopen(panel)
await expect
.element(reopened.getByRole('textbox', { name: 'Agent task', exact: true }))
.toHaveTextContent('Review the renewal terms and highlight unusual liability clauses.')
})
it('retains the wait limit and error strategy through panel reopening', async () => {
const { panel } = await setup()
const enabled = panel.getByRole('switch', { name: 'External agent wait limit', exact: true })
const minutes = panel.getByRole('textbox', { name: 'Wait limit', exact: true })
await expect.element(enabled).toBeChecked()
await expect.element(minutes).toHaveValue('10')
await minutes.fill('25')
await userEvent.tab()
await expect
.element(panel.getByRole('slider', { name: 'Wait limit', exact: true }))
.toHaveAttribute('aria-valuenow', '25')
await enabled.click()
await expect.element(enabled).not.toBeChecked()
await enabled.click()
await expect.element(minutes).toHaveValue('25')
const errorHandling = panel.getByRole('button', { name: 'Fail Branch', exact: true })
await expect.element(errorHandling).toBeVisible()
await errorHandling.click()
await page.getByRole('menuitemradio', { name: /^None/ }).click()
const reopened = await closeAndReopen(panel)
await expect
.element(reopened.getByRole('textbox', { name: 'Wait limit', exact: true }))
.toHaveValue('25')
await expect.element(reopened.getByRole('button', { name: 'None', exact: true })).toBeVisible()
await expect
.element(reopened.getByRole('combobox', { name: 'On timeout', exact: true }))
.toMatchTextContent('End waiting')
})
it('creates an output token from the task editor and retains its output definition', async () => {
const { panel } = await setup()
await replaceTask(panel.getByRole('textbox', { name: 'Agent task', exact: true }), 'Return ')
await userEvent.keyboard('/')
await page
.getByRole('listbox', { name: 'Typeahead menu', exact: true })
.getByRole('button', { name: 'New output', exact: true })
.click()
const name = page.getByRole('textbox', { name: 'Field name', exact: true })
await expect.element(name).toHaveFocus()
await name.fill('review_notes')
await userEvent.keyboard('{Enter}')
await expect.element(name).not.toBeInTheDocument()
await expect
.element(panel.getByRole('textbox', { name: 'Agent task', exact: true }))
.toMatchTextContent(/review_notes/)
const reopened = await closeAndReopen(panel)
await expect
.element(reopened.getByRole('textbox', { name: 'Agent task', exact: true }))
.toMatchTextContent(/review_notes/)
await expect
.element(reopened.getByRole('button', { name: 'Delete review_notes', exact: true }))
.toBeInTheDocument()
})
it('allows the default text output to be edited and deleted while retaining a newly added output', async () => {
const { panel } = await setup()
await expect.element(panel.getByText('Text returned by the agent', { exact: true })).toBeVisible()
await panel.getByText('text', { exact: true }).last().hover()
await panel.getByRole('button', { name: 'Edit text', exact: true }).click()
await panel
.getByRole('form', { name: 'Output variable editor', exact: true })
.getByRole('textbox', { name: 'Description', exact: true })
.fill('Contract review result')
await panel.getByRole('button', { name: 'Confirm', exact: true }).click()
await expect.element(panel.getByText('Contract review result', { exact: true })).toBeVisible()
await panel.getByText('text', { exact: true }).last().hover()
await panel.getByRole('button', { name: 'Edit text', exact: true }).click()
const name = panel.getByRole('textbox', { name: 'Field name', exact: true })
await name.fill('summary')
await panel.getByRole('button', { name: 'Confirm', exact: true }).click()
await expect.element(panel.getByText('summary', { exact: true })).toBeVisible()
await panel.getByRole('button', { name: 'New output', exact: true }).click()
await name.fill('review_notes')
await panel.getByRole('button', { name: 'Confirm', exact: true }).click()
await panel.getByText('summary', { exact: true }).hover()
await panel.getByRole('button', { name: 'Delete summary', exact: true }).click()
await expect.element(panel.getByText('summary', { exact: true })).not.toBeInTheDocument()
const reopened = await closeAndReopen(panel)
await expect.element(reopened.getByText('review_notes', { exact: true })).toBeVisible()
await expect.element(reopened.getByText('text', { exact: true })).not.toBeInTheDocument()
})
it('shows the empty Last Run tab without requesting an execution record', async () => {
const { panel } = await setup()
await panel.getByRole('tab', { name: 'Last Run', exact: true }).click()
await expect
.element(panel.getByText('The results of the last run will be displayed here', { exact: true }))
.toBeVisible()
await panel.getByRole('tab', { name: 'Settings', exact: true }).click()
await expect
.element(panel.getByRole('textbox', { name: 'Agent task', exact: true }))
.toBeVisible()
})
@@ -0,0 +1,126 @@
import type { ExternalAgentWorkflowPreviewAgent } from './types'
import { Button } from '@langgenius/dify-ui/button'
import { Popover, PopoverContent, PopoverTitle, PopoverTrigger } from '@langgenius/dify-ui/popover'
import { useId, useState } from 'react'
import { useTranslation } from 'react-i18next'
import AppIcon from '@/app/components/base/app-icon'
import { SearchInput } from '@/app/components/base/search-input'
import { externalWorkflowAgentExamples } from '@/app/components/workflow/block-selector/workflow-agent-selector-preview'
import { ExternalAgentBadge } from './external-badge'
export function ExternalAgentField({
agent,
onChange,
}: {
agent: ExternalAgentWorkflowPreviewAgent
onChange: (agent: ExternalAgentWorkflowPreviewAgent) => void
}) {
const { t } = useTranslation(['workflow', 'workflowAgent', 'agentRoster'])
const labelId = useId()
const [open, setOpen] = useState(false)
return (
<Popover open={open} onOpenChange={setOpen}>
<div role="group" aria-labelledby={labelId} className="px-4 pt-4 pb-2">
<div className="mb-1 flex h-6 items-center justify-between gap-2">
<span id={labelId} className="system-sm-semibold-uppercase text-text-secondary">
{t(($) => $['nodes.agent.roster.label'], { ns: 'workflow' })}
</span>
<PopoverTrigger
render={<Button variant="ghost" size="small" className="text-text-tertiary" />}
>
{t(($) => $['nodes.agent.roster.change'], { ns: 'workflowAgent' })}
</PopoverTrigger>
</div>
<PopoverTrigger
render={
<Button
variant="ghost"
className="h-8 w-full justify-start gap-1 rounded-lg bg-components-input-bg-normal p-1 text-start hover:bg-state-base-hover-alt"
/>
}
>
<span className="flex size-6 shrink-0 items-center justify-center">
<AppIcon
size="tiny"
className="size-5 rounded-md"
icon={agent.icon}
fallback={{
content: (
<span aria-hidden className="i-ri-robot-2-line size-3.5 text-text-accent" />
),
background: 'var(--color-background-default)',
}}
/>
</span>
<span className="truncate system-sm-regular text-components-input-text-filled">
{agent.name}
</span>
<ExternalAgentBadge />
<span
aria-hidden
className="ms-auto me-1 i-ri-arrow-right-s-line size-4 shrink-0 text-text-tertiary"
/>
</PopoverTrigger>
</div>
<PopoverContent align="start" className="w-100 max-w-[calc(100vw-2rem)] p-1">
<PopoverTitle className="sr-only">
{t(($) => $['roster.nodeSelector.dialogLabel'], { ns: 'agentRoster' })}
</PopoverTitle>
<ExternalAgentOptions
onSelect={(next) => {
onChange(next)
setOpen(false)
}}
/>
</PopoverContent>
</Popover>
)
}
function ExternalAgentOptions({
onSelect,
}: {
onSelect: (agent: ExternalAgentWorkflowPreviewAgent) => void
}) {
const { t } = useTranslation(['agentRoster'])
const [search, setSearch] = useState('')
const agents = externalWorkflowAgentExamples.filter((agent) =>
agent.name.toLowerCase().includes(search.trim().toLowerCase()),
)
return (
<div>
<div className="p-1">
<SearchInput
value={search}
onValueChange={setSearch}
aria-label={t(($) => $['roster.searchLabel'])}
placeholder={t(($) => $['roster.nodeSelector.searchAgentsPlaceholder'])}
/>
</div>
<div className="flex max-h-60 flex-col overflow-y-auto p-1">
{agents.map((agent) => (
<Button
key={agent.key}
variant="ghost"
disabled={!agent.enabled}
className="h-8 w-full shrink-0 justify-start gap-2"
onClick={() => onSelect(agent)}
>
<AppIcon size="tiny" icon={agent.icon} />
<span className="truncate system-sm-medium">{agent.name}</span>
<span className="system-xs-regular text-text-quaternary">
{agent.enabled
? t(($) => $['roster.nodeSelector.source.external'])
: t(($) => $['external.filters.disabled'])}
</span>
</Button>
))}
{agents.length === 0 && (
<p role="status" className="p-3 system-sm-regular text-text-tertiary">
{t(($) => $['roster.emptySearch'])}
</p>
)}
</div>
</div>
)
}
@@ -0,0 +1,10 @@
import { useTranslation } from 'react-i18next'
export function ExternalAgentBadge() {
const { t } = useTranslation(['agentRoster'])
return (
<span className="shrink-0 rounded-[5px] border border-divider-deep px-1.25 py-0.5 system-2xs-medium-uppercase text-text-tertiary">
{t(($) => $['roster.nodeSelector.source.external'])}
</span>
)
}
@@ -0,0 +1,220 @@
import { Field, FieldLabel } from '@langgenius/dify-ui/field'
import { Infotip, InfotipContent, InfotipTrigger } from '@langgenius/dify-ui/infotip'
import {
NumberField,
NumberFieldGroup,
NumberFieldInput,
NumberFieldUnit,
} from '@langgenius/dify-ui/number-field'
import {
Select,
SelectContent,
SelectItem,
SelectItemIndicator,
SelectItemText,
SelectLabel,
SelectTrigger,
} from '@langgenius/dify-ui/select'
import { Separator } from '@langgenius/dify-ui/separator'
import {
Slider,
SliderControl,
SliderIndicator,
SliderLabel,
SliderThumb,
SliderTrack,
} from '@langgenius/dify-ui/slider'
import { Switch } from '@langgenius/dify-ui/switch'
import { useId, useState } from 'react'
import { useTranslation } from 'react-i18next'
import {
Collapse,
CollapseActions,
CollapseContent,
CollapseHeader,
CollapseIndicator,
CollapseTitle,
CollapseTrigger,
} from '@/app/components/workflow/nodes/_base/components/collapse'
import ErrorHandleTypeSelector from '@/app/components/workflow/nodes/_base/components/error-handle/error-handle-type-selector'
import FailBranchCard from '@/app/components/workflow/nodes/_base/components/error-handle/fail-branch-card'
import { ErrorHandleTypeEnum } from '@/app/components/workflow/nodes/_base/components/error-handle/types'
type ExternalAgentWorkflowSettingsProps = {
waitLimitEnabled: boolean
waitLimitMinutes: number
onWaitLimitEnabledChange: (enabled: boolean) => void
onWaitLimitMinutesChange: (minutes: number) => void
errorStrategy: ErrorHandleTypeEnum
onErrorStrategyChange: (strategy: ErrorHandleTypeEnum) => void
}
export function ExternalAgentWorkflowSettings({
waitLimitEnabled,
waitLimitMinutes,
onWaitLimitEnabledChange,
onWaitLimitMinutesChange,
errorStrategy,
onErrorStrategyChange,
}: ExternalAgentWorkflowSettingsProps) {
const { t } = useTranslation(['agentRoster', 'workflow'])
const waitLimitTitleId = useId()
const waitLimitLabelId = useId()
const timeoutHelpId = useId()
const errorHandlingTitleId = useId()
const [errorHandlingCollapsed, setErrorHandlingCollapsed] = useState(false)
const waitLimitLabel = t(($) => $['external.workflow.waitLimit.label'])
const waitLimitHelp = t(($) => $['external.workflow.waitLimit.help'])
const hasErrorStrategy = errorStrategy !== ErrorHandleTypeEnum.none
const changeWaitLimit = (minutes: number | null) => {
if (minutes !== null && Number.isFinite(minutes) && minutes > 0)
onWaitLimitMinutesChange(minutes)
}
return (
<>
<section className="px-4 py-4">
<Field name="external_agent_wait_limit_enabled" className="gap-0">
<div className="flex min-h-6 items-center gap-1">
<FieldLabel
id={waitLimitTitleId}
className="min-w-0 py-1 system-sm-semibold-uppercase text-text-secondary"
>
{t(($) => $['external.workflow.waitLimit.title'])}
</FieldLabel>
<Infotip>
<InfotipTrigger aria-labelledby={waitLimitTitleId} />
<InfotipContent aria-labelledby={waitLimitTitleId}>{waitLimitHelp}</InfotipContent>
</Infotip>
<Switch
aria-labelledby={waitLimitTitleId}
checked={waitLimitEnabled}
onCheckedChange={onWaitLimitEnabledChange}
className="ms-auto"
/>
</div>
</Field>
{waitLimitEnabled && (
<div className="mt-2 space-y-2">
<div className="flex min-h-8 items-center justify-between gap-3">
<span
id={waitLimitLabelId}
className="system-xs-regular text-text-tertiary uppercase"
>
{waitLimitLabel}
</span>
<div className="flex shrink-0 items-center gap-3">
<Slider<number>
value={waitLimitMinutes}
min={Math.min(1, waitLimitMinutes)}
max={Math.max(30, waitLimitMinutes)}
step={1}
onValueChange={changeWaitLimit}
className="w-27"
>
<SliderLabel className="sr-only">{waitLimitLabel}</SliderLabel>
<SliderControl>
<SliderTrack>
<SliderIndicator />
<SliderThumb />
</SliderTrack>
</SliderControl>
</Slider>
<Field name="external_agent_wait_limit_minutes" className="w-20 gap-0">
<FieldLabel className="sr-only">{waitLimitLabel}</FieldLabel>
<NumberField
value={waitLimitMinutes}
min={0}
step={1}
format={{ useGrouping: false }}
onValueChange={changeWaitLimit}
>
<NumberFieldGroup>
<NumberFieldInput aria-labelledby={waitLimitLabelId} className="ps-2 pe-1" />
<NumberFieldUnit>
{t(($) => $['external.workflow.waitLimit.unit'])}
</NumberFieldUnit>
</NumberFieldGroup>
</NumberField>
</Field>
</div>
</div>
<Select<'end-waiting'> value="end-waiting">
<div className="flex min-h-8 items-center justify-between gap-3">
<SelectLabel className="py-0 system-xs-regular text-text-tertiary uppercase">
{t(($) => $['external.workflow.waitLimit.onTimeout'])}
</SelectLabel>
<SelectTrigger aria-describedby={timeoutHelpId} className="w-50 shrink-0">
{t(($) => $['external.workflow.waitLimit.endWaiting'])}
</SelectTrigger>
</div>
<SelectContent>
<SelectItem value="end-waiting">
<SelectItemText>
{t(($) => $['external.workflow.waitLimit.endWaiting'])}
</SelectItemText>
<SelectItemIndicator />
</SelectItem>
</SelectContent>
</Select>
<p id={timeoutHelpId} className="system-xs-regular text-text-tertiary">
{waitLimitHelp}
</p>
</div>
)}
</section>
<Separator decorative className="my-0 bg-divider-subtle" />
<section className="py-4">
<Collapse
collapsed={errorHandlingCollapsed}
onCollapse={setErrorHandlingCollapsed}
disabled={!hasErrorStrategy}
>
<CollapseHeader>
<CollapseTitle className="ms-4">
<span id={errorHandlingTitleId}>
{t(($) => $['nodes.common.errorHandle.title'], { ns: 'workflow' })}
</span>
</CollapseTitle>
<Infotip>
<InfotipTrigger aria-labelledby={errorHandlingTitleId} />
<InfotipContent aria-labelledby={errorHandlingTitleId}>
{t(($) => $['nodes.common.errorHandle.tip'], { ns: 'workflow' })}
</InfotipContent>
</Infotip>
{hasErrorStrategy && (
<CollapseTrigger
aria-labelledby={errorHandlingTitleId}
className="ms-0 size-6 justify-center"
>
<CollapseIndicator />
</CollapseTrigger>
)}
<CollapseActions>
<div className="pe-4">
<ErrorHandleTypeSelector
value={errorStrategy}
onSelected={(strategy) => {
setErrorHandlingCollapsed(false)
onErrorStrategyChange(strategy)
}}
/>
</div>
</CollapseActions>
</CollapseHeader>
<CollapseContent>
{errorStrategy === ErrorHandleTypeEnum.failBranch && (
<FailBranchCard learnMorePlacement="below" />
)}
{errorStrategy === ErrorHandleTypeEnum.defaultValue && (
<p className="px-4 pt-2 system-xs-regular text-text-tertiary">
{t(($) => $['nodes.common.errorHandle.defaultValue.desc'], { ns: 'workflow' })}
</p>
)}
</CollapseContent>
</Collapse>
</section>
</>
)
}
@@ -0,0 +1,40 @@
import type { ExternalAgentWorkflowPreviewNodeData } from './types'
import { useTranslation } from 'react-i18next'
import AppIcon from '@/app/components/base/app-icon'
import { ExternalAgentBadge } from './external-badge'
export function ExternalAgentWorkflowPreviewNode({
data,
}: {
data: ExternalAgentWorkflowPreviewNodeData
}) {
const { t } = useTranslation(['workflow'])
const agent = data._externalAgentPreview
return (
<div className="flex flex-col gap-0.5 px-2.5 py-1">
<div className="py-0.5 system-2xs-medium-uppercase text-text-tertiary">
{t(($) => $['nodes.agent.roster.label'])}
</div>
<div className="flex min-w-0 items-center gap-2 rounded-lg bg-workflow-block-parma-bg p-1">
<AppIcon
size="small"
icon={agent.icon}
fallback={{
content: <span aria-hidden className="i-ri-robot-2-line size-4 text-text-tertiary" />,
background: 'var(--color-components-icon-bg-midnight-soft)',
}}
/>
<div className="min-w-0 flex-1">
<div className="flex items-center gap-1">
<span className="truncate system-xs-regular text-text-secondary">{agent.name}</span>
<ExternalAgentBadge />
</div>
{agent.role && (
<div className="truncate system-2xs-regular text-text-tertiary">{agent.role}</div>
)}
</div>
</div>
</div>
)
}
@@ -0,0 +1,227 @@
import type { ExternalAgentWorkflowPreviewNodeData } from './types'
import type { CommonNodeType } from '@/app/components/workflow/types'
import { IconButton } from '@langgenius/dify-ui/icon-button'
import { Menu, MenuContent, MenuItem, MenuTrigger } from '@langgenius/dify-ui/menu'
import { Tabs, TabsList, TabsPanel, TabsTab } from '@langgenius/dify-ui/tabs'
import { Tooltip, TooltipContent, TooltipTrigger } from '@langgenius/dify-ui/tooltip'
import { useCallback, useId, useRef, useState } from 'react'
import { useTranslation } from 'react-i18next'
import { useReactFlow } from 'reactflow'
import BlockIcon from '@/app/components/workflow/block-icon'
import { ErrorHandleTypeEnum } from '@/app/components/workflow/nodes/_base/components/error-handle/types'
import { TitleInput } from '@/app/components/workflow/nodes/_base/components/title-description-input'
import NoLastRunData from '@/app/components/workflow/nodes/_base/components/workflow-panel/last-run/no-data'
import { AgentOutputVariables } from '@/app/components/workflow/nodes/agent-v2/components/agent-output-variables'
import { BlockEnum } from '@/app/components/workflow/types'
import { ExternalAgentField } from './agent-field'
import { ExternalAgentWorkflowSettings } from './external-settings'
import { externalAgentDefaultOutputs, externalAgentExampleTask } from './preview'
import { ExternalAgentTaskField } from './task-field'
import { isExternalAgentWorkflowPreviewData } from './types'
const externalReservedOutputNames: ReadonlySet<string> = new Set()
type PreviewUpdate =
| Partial<ExternalAgentWorkflowPreviewNodeData>
| ((
current: ExternalAgentWorkflowPreviewNodeData,
) => Partial<ExternalAgentWorkflowPreviewNodeData>)
export function ExternalAgentWorkflowPreviewPanel({
id,
data,
}: {
id: string
data: ExternalAgentWorkflowPreviewNodeData
}) {
const { t } = useTranslation(['agentRoster', 'common', 'workflow', 'workflowDebug'])
const { setNodes, setEdges } = useReactFlow<CommonNodeType>()
const [tab, setTab] = useState<'settings' | 'lastRun'>('settings')
const [outputsCollapsed, setOutputsCollapsed] = useState(false)
const titleContainerRef = useRef<HTMLDivElement>(null)
const renameRequestedRef = useRef(false)
const descriptionId = useId()
const agent = data._externalAgentPreview
const task = data.agent_task ?? externalAgentExampleTask
const outputs = data.agent_declared_outputs ?? externalAgentDefaultOutputs
const waitLimit = data._externalAgentWaitLimit ?? { enabled: true, minutes: 10 }
const update = useCallback(
(patch: PreviewUpdate) => {
// Preview configuration belongs to this local canvas node. It does not enter draft sync
// or the collaboration writer; those boundaries also exclude temporary external nodes.
setNodes((nodes) =>
nodes.map((node) => {
if (node.id !== id || !isExternalAgentWorkflowPreviewData(node.data)) return node
const changes = typeof patch === 'function' ? patch(node.data) : patch
return {
...node,
...(typeof changes.selected === 'boolean' ? { selected: changes.selected } : {}),
data: { ...node.data, ...changes },
}
}),
)
},
[id, setNodes],
)
const close = () => update({ selected: false })
const remove = () => {
setNodes((nodes) => nodes.filter((node) => node.id !== id))
setEdges((edges) => edges.filter((edge) => edge.source !== id && edge.target !== id))
}
const runLabel = t(($) => $['panel.runThisStep'], { ns: 'workflow' })
return (
<section
aria-label={agent.name}
className="me-1 flex h-full w-100 max-w-full shrink-0 flex-col overflow-hidden rounded-2xl border-[0.5px] border-components-panel-border bg-components-panel-bg shadow-lg"
>
<Tabs
value={tab}
onValueChange={(value) => {
if (value === 'settings' || value === 'lastRun') setTab(value)
}}
className="flex min-h-0 flex-1 flex-col"
>
<header className="shrink-0 border-b border-divider-subtle px-4 pt-4">
<div className="flex h-7 items-center gap-1">
<BlockIcon type={BlockEnum.AgentV2} size="md" />
<div ref={titleContainerRef} className="min-w-0 flex-1">
<TitleInput value={data.title || agent.name} onBlur={(title) => update({ title })} />
</div>
<Tooltip>
<TooltipTrigger
render={
<IconButton aria-label={runLabel} disabled focusableWhenDisabled>
<span aria-hidden className="i-ri-play-line size-4" />
</IconButton>
}
/>
<TooltipContent>{t(($) => $['external.workflow.runUnavailable'])}</TooltipContent>
</Tooltip>
<Menu
onOpenChange={(open) => {
if (open) renameRequestedRef.current = false
}}
>
<MenuTrigger
render={
<IconButton aria-label={t(($) => $['operation.more'], { ns: 'common' })}>
<span aria-hidden className="i-ri-more-fill size-4" />
</IconButton>
}
/>
<MenuContent
finalFocus={() =>
renameRequestedRef.current
? (titleContainerRef.current?.querySelector('input') ?? true)
: true
}
>
<MenuItem
onClick={() => {
renameRequestedRef.current = true
}}
>
<span aria-hidden className="i-ri-pencil-line size-4" />
{t(($) => $['operation.rename'], { ns: 'common' })}
</MenuItem>
<MenuItem onClick={remove} className="text-text-destructive">
<span aria-hidden className="i-ri-delete-bin-line size-4" />
{t(($) => $['operation.delete'], { ns: 'common' })}
</MenuItem>
</MenuContent>
</Menu>
<span aria-hidden className="mx-2 h-3.5 w-px bg-divider-subtle" />
<IconButton
aria-label={t(($) => $['operation.close'], { ns: 'common' })}
onClick={close}
>
<span aria-hidden className="i-ri-close-line size-4" />
</IconButton>
</div>
<label htmlFor={descriptionId} className="sr-only">
{t(($) => $['common.nodeDescription'], { ns: 'workflow' })}
</label>
<textarea
id={descriptionId}
rows={1}
value={data.desc || ''}
onChange={(event) => update({ desc: event.target.value })}
placeholder={t(($) => $['common.addDescription'], { ns: 'workflow' })}
className="mt-1 block h-5 w-full resize-none rounded-sm bg-transparent py-0.5 system-xs-regular text-text-secondary outline-hidden placeholder:text-text-quaternary focus-visible:ring-1 focus-visible:ring-components-input-border-hover"
/>
<TabsList aria-label={t(($) => $['external.overview.type'])} className="mt-0.5 h-8 gap-4">
<TabsTab
value="settings"
className="border-b-2 py-1 system-sm-medium uppercase data-active:text-text-primary"
>
{t(($) => $['debug.settingsTab'], { ns: 'workflowDebug' })}
</TabsTab>
<TabsTab value="lastRun" className="border-b-2 py-1 system-sm-medium uppercase">
{t(($) => $['debug.lastRunTab'], { ns: 'workflowDebug' })}
</TabsTab>
</TabsList>
</header>
<TabsPanel value="settings" className="min-h-0 flex-1 overflow-y-auto pb-4">
<ExternalAgentField
agent={agent}
onChange={(next) =>
update((current) => ({
_externalAgentPreview: next,
title:
current.title === current._externalAgentPreview.name ? next.name : current.title,
}))
}
/>
<ExternalAgentTaskField
id={id}
value={task}
outputs={outputs}
onChange={(agent_task) => update({ agent_task })}
onOutputsChange={(agent_declared_outputs, agent_task) =>
update({
agent_declared_outputs,
...(agent_task === undefined ? {} : { agent_task }),
})
}
/>
<div className="border-b border-divider-subtle">
<AgentOutputVariables
compact
reservedOutputNames={externalReservedOutputNames}
outputs={outputs}
collapsed={outputsCollapsed}
onCollapse={setOutputsCollapsed}
onChange={(agent_declared_outputs) => update({ agent_declared_outputs })}
/>
</div>
<ExternalAgentWorkflowSettings
waitLimitEnabled={waitLimit.enabled}
waitLimitMinutes={waitLimit.minutes}
onWaitLimitEnabledChange={(enabled) =>
update((current) => ({
_externalAgentWaitLimit: {
...(current._externalAgentWaitLimit ?? waitLimit),
enabled,
},
}))
}
onWaitLimitMinutesChange={(minutes) =>
update((current) => ({
_externalAgentWaitLimit: {
...(current._externalAgentWaitLimit ?? waitLimit),
minutes,
},
}))
}
errorStrategy={data.error_strategy ?? ErrorHandleTypeEnum.failBranch}
onErrorStrategyChange={(error_strategy) => update({ error_strategy })}
/>
</TabsPanel>
<TabsPanel value="lastRun" className="flex min-h-0 flex-1 flex-col">
<NoLastRunData canSingleRun={false} onSingleRun={() => {}} />
</TabsPanel>
</Tabs>
</section>
)
}
@@ -0,0 +1,36 @@
import type {
ExternalAgentWorkflowPreviewAgent,
ExternalAgentWorkflowPreviewDefaultValue,
} from './types'
import { ErrorHandleTypeEnum } from '@/app/components/workflow/nodes/_base/components/error-handle/types'
export const externalAgentExampleTask =
'Review this contract against the company’s payment policy. Identify risks and suggest safer wording.\nPolicy: {{#policy.result#}}\nContract: {{#input.contract_text#}}'
export const externalAgentDefaultOutputs = [
{
name: 'text',
type: 'string' as const,
required: false,
description: 'Text returned by the agent',
},
]
// The external-agent workflow contract is not available yet. Keep this selection on the
// current canvas only; draft serialization excludes temporary nodes and their edges.
export function createExternalAgentWorkflowPreviewData(
agent: ExternalAgentWorkflowPreviewAgent,
): ExternalAgentWorkflowPreviewDefaultValue {
return {
title: agent.name,
agent_node_kind: 'external_agent',
version: '2',
agent_binding: undefined,
_isTempNode: true,
_externalAgentPreview: { ...agent },
agent_task: externalAgentExampleTask,
agent_declared_outputs: externalAgentDefaultOutputs.map((output) => ({ ...output })),
_externalAgentWaitLimit: { enabled: true, minutes: 10 },
error_strategy: ErrorHandleTypeEnum.failBranch,
}
}
@@ -0,0 +1,154 @@
import type { DeclaredOutputConfig } from '@dify/contracts/api/console/apps/types.gen'
import type { WorkflowNodesMap } from '@/app/components/base/prompt-editor/types'
import type { NodeOutPutVar } from '@/app/components/workflow/types'
import { cn } from '@langgenius/dify-ui/cn'
import { Field, FieldLabel } from '@langgenius/dify-ui/field'
import { Infotip, InfotipContent, InfotipTrigger } from '@langgenius/dify-ui/infotip'
import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext'
import { $insertNodes } from 'lexical'
import { useId, useState } from 'react'
import { useTranslation } from 'react-i18next'
import PromptEditor from '@/app/components/base/prompt-editor'
import { $createCustomTextNode } from '@/app/components/base/prompt-editor/plugins/custom-text/node'
import { useWorkflowVariableType } from '@/app/components/workflow/hooks/use-workflow-variables'
import useAvailableVarList from '@/app/components/workflow/nodes/_base/hooks/use-available-var-list'
import { Type } from '@/app/components/workflow/nodes/llm/types'
import { BlockEnum, VarType } from '@/app/components/workflow/types'
const exampleVariables: NodeOutPutVar[] = [
{
nodeId: 'policy',
title: 'Knowledge Retrieval',
vars: [{ variable: 'result', type: VarType.string }],
},
{
nodeId: 'input',
title: 'User Input',
vars: [{ variable: 'contract_text', type: VarType.string }],
},
]
const exampleNodes: WorkflowNodesMap = {
policy: { title: 'Knowledge Retrieval', type: BlockEnum.KnowledgeRetrieval },
input: { title: 'User Input', type: BlockEnum.Start },
}
function TaskToolbar({ value, focused }: { value: string; focused: boolean }) {
const { t } = useTranslation(['workflowAgent', 'agentRoster'])
const [editor] = useLexicalComposerContext()
return (
<div className="flex h-8 shrink-0 items-center justify-between px-3">
{focused ? (
<button
type="button"
className="flex items-center gap-1 rounded-sm system-xs-medium text-text-tertiary focus-visible:outline-2 focus-visible:outline-state-accent-solid"
onMouseDown={(event) => event.preventDefault()}
onClick={() => {
editor.focus()
editor.update(() => $insertNodes([$createCustomTextNode('/')]))
}}
>
<span aria-hidden className="i-ri-slash-commands-2 size-3.5" />
{t(($) => $['nodes.agent.task.insert'], { ns: 'workflowAgent' })}
</button>
) : (
<span />
)}
<span className="rounded-[5px] border border-divider-deep px-1 system-2xs-medium text-text-tertiary">
{t(($) => $['external.workflow.task.characterCount'], {
ns: 'agentRoster',
count: value.length,
})}
</span>
</div>
)
}
export function ExternalAgentTaskField({
id,
value,
outputs,
onChange,
onOutputsChange,
}: {
id: string
value: string
outputs: DeclaredOutputConfig[]
onChange: (value: string) => void
onOutputsChange: (outputs: DeclaredOutputConfig[], task?: string) => void
}) {
const { t } = useTranslation(['workflowAgent'])
const labelId = useId()
const [focused, setFocused] = useState(false)
const { availableVars, availableNodesWithParent } = useAvailableVarList(id)
const getWorkflowVarType = useWorkflowVariableType()
const missingExamples = exampleVariables.filter(
(example) => !availableVars.some((item) => item.nodeId === example.nodeId),
)
const variables = [...availableVars, ...missingExamples]
const workflowNodesMap = { ...exampleNodes }
for (const node of availableNodesWithParent)
workflowNodesMap[node.id] = {
title: node.data.title,
type: node.data.type,
width: node.width ?? undefined,
height: node.height ?? undefined,
position: node.position,
}
return (
<Field name="agent_task" className="gap-1 px-4 py-2">
<div className="flex h-6 items-center gap-1">
<FieldLabel id={labelId} className="system-sm-semibold-uppercase! text-text-secondary">
{t(($) => $['nodes.agent.task.label'])}
</FieldLabel>
<Infotip>
<InfotipTrigger aria-labelledby={labelId} />
<InfotipContent aria-labelledby={labelId}>
{t(($) => $['nodes.agent.task.tooltip'])}
</InfotipContent>
</Infotip>
</div>
<div
className={cn(
'h-30 rounded-[10px] p-0.5',
focused
? 'bg-linear-to-r from-components-input-border-active-prompt-1 to-components-input-border-active-prompt-2'
: 'bg-transparent',
)}
>
<div
className={cn(
'flex h-full flex-col rounded-lg',
focused ? 'bg-background-default' : 'bg-components-input-bg-normal',
)}
>
<PromptEditor
aria-label={t(($) => $['nodes.agent.task.label'])}
wrapperClassName="flex h-full flex-col"
className="min-h-0 flex-1 overflow-y-auto px-3 py-2"
placeholderClassName="px-3 py-2"
placeholder={t(($) => $['nodes.agent.task.placeholder'])}
value={value}
onChange={onChange}
compact
editable
onFocus={() => setFocused(true)}
onBlur={() => setFocused(false)}
workflowVariableBlock={{
show: true,
variables,
workflowNodesMap,
getVarType: (payload) =>
missingExamples.some((example) => example.nodeId === payload.nodeId)
? Type.string
: getWorkflowVarType(payload),
}}
isSupportFileVar
agentOutputBlock={{ show: true, outputs, onChange: onOutputsChange }}
>
<TaskToolbar value={value} focused={focused} />
</PromptEditor>
</div>
</div>
</Field>
)
}
@@ -0,0 +1,38 @@
import type { DeclaredOutputConfig } from '@dify/contracts/api/console/apps/types.gen'
import type { CommonNodeType } from '@/app/components/workflow/types'
import type { ExternalAgentListItem } from '@/features/agent-v2/roster/components/external-agent-list'
import { BlockEnum } from '@/app/components/workflow/types'
export type ExternalAgentWorkflowPreviewAgent = Pick<
ExternalAgentListItem,
'key' | 'name' | 'role' | 'description' | 'icon' | 'enabled'
>
export type ExternalAgentWorkflowPreviewDefaultValue = {
title?: string
agent_node_kind: 'external_agent'
version: '2'
agent_binding: undefined
_isTempNode: true
_externalAgentPreview: ExternalAgentWorkflowPreviewAgent
agent_task?: string
agent_declared_outputs?: DeclaredOutputConfig[]
_externalAgentWaitLimit?: { enabled: boolean; minutes: number }
error_strategy?: CommonNodeType['error_strategy']
}
export type ExternalAgentWorkflowPreviewNodeData = CommonNodeType &
ExternalAgentWorkflowPreviewDefaultValue
export function isExternalAgentWorkflowPreviewData(
data: CommonNodeType,
): data is ExternalAgentWorkflowPreviewNodeData {
const preview = data as Partial<ExternalAgentWorkflowPreviewNodeData>
return (
(data.type === BlockEnum.Agent || data.type === BlockEnum.AgentV2) &&
preview.agent_node_kind === 'external_agent' &&
preview.version === '2' &&
preview._isTempNode === true &&
!!preview._externalAgentPreview
)
}
+15
View File
@@ -152,6 +152,15 @@
"external.references": "التطبيقات التي تستخدم {{name}}",
"external.searchPlaceholder": "البحث عن وكلاء…",
"external.updated": "تم التحديث {{time}}",
"external.workflow.runUnavailable": "تشغيلات الوكيل الخارجي غير متاحة بعد.",
"external.workflow.task.characterCount_one": "{{count}} حرف",
"external.workflow.task.characterCount_other": "{{count}} حرفًا",
"external.workflow.waitLimit.endWaiting": "إنهاء الانتظار",
"external.workflow.waitLimit.help": "طبّق معالجة الأخطاء المكوّنة. قد تستمر مهمة الوكيل في العمل.",
"external.workflow.waitLimit.label": "حد الانتظار",
"external.workflow.waitLimit.onTimeout": "عند انتهاء المهلة",
"external.workflow.waitLimit.title": "حد انتظار الوكيل الخارجي",
"external.workflow.waitLimit.unit": "دقيقة",
"roster.createAgent": "إنشاء وكيل",
"roster.createDialog.description": "أنشئ Agent قابلاً لإعادة الاستخدام في مساحة العمل هذه.",
"roster.createDialog.title": "إنشاء وكيل",
@@ -190,8 +199,14 @@
"roster.nodeSelector.createInlineFailed": "فشل إنشاء وكيل inline. قم بالتحديث وحاول مرة أخرى.",
"roster.nodeSelector.dialogLabel": "اختر وكيلاً",
"roster.nodeSelector.manageInAgentConsole": "إدارة في Agents",
"roster.nodeSelector.searchAgentsPlaceholder": "البحث عن الوكلاء…",
"roster.nodeSelector.searchPlaceholder": "بحث…",
"roster.nodeSelector.source.dify": "Dify",
"roster.nodeSelector.source.external": "خارجي",
"roster.nodeSelector.startFromScratch": "البدء من الصفر",
"roster.nodeSelector.tabs.all": "الكل",
"roster.nodeSelector.tabs.dify": "Dify",
"roster.nodeSelector.tabs.external": "خارجي",
"roster.references.label": "سير العمل الذي يستخدم {{name}}",
"roster.references.trigger": "سير العمل الذي يستخدم {{name}}",
"roster.saveToRoster": "حفظ إلى Agents",
+15
View File
@@ -144,6 +144,15 @@
"external.references": "{{name}} istifadə edən tətbiqlər",
"external.searchPlaceholder": "Agentləri axtar…",
"external.updated": "Yenilənib: {{time}}",
"external.workflow.runUnavailable": "Xarici agent işə salınmaları hələ əlçatan deyil.",
"external.workflow.task.characterCount_one": "{{count}} simvol",
"external.workflow.task.characterCount_other": "{{count}} simvol",
"external.workflow.waitLimit.endWaiting": "Gözləməni bitir",
"external.workflow.waitLimit.help": "Konfiqurasiya edilmiş xəta emalını tətbiq edin. Agentin tapşırığı icra olunmağa davam edə bilər.",
"external.workflow.waitLimit.label": "Gözləmə limiti",
"external.workflow.waitLimit.onTimeout": "Vaxt bitdikdə",
"external.workflow.waitLimit.title": "Xarici agent gözləmə limiti",
"external.workflow.waitLimit.unit": "dəq",
"roster.createAgent": "Agent yarat",
"roster.createDialog.description": "Bu iş sahəsində təkrar istifadə edilə bilən agent yaradın.",
"roster.createDialog.title": "Agentin yaradılması",
@@ -182,8 +191,14 @@
"roster.nodeSelector.createInlineFailed": "Daxili agent yaratmaq mümkün olmadı. Səhifəni yeniləyin və yenidən cəhd edin.",
"roster.nodeSelector.dialogLabel": "Agent seçimi",
"roster.nodeSelector.manageInAgentConsole": "Agentlər bölməsində idarə et",
"roster.nodeSelector.searchAgentsPlaceholder": "Agentləri axtarın…",
"roster.nodeSelector.searchPlaceholder": "Axtar…",
"roster.nodeSelector.source.dify": "Dify",
"roster.nodeSelector.source.external": "Xarici",
"roster.nodeSelector.startFromScratch": "Sıfırdan başla",
"roster.nodeSelector.tabs.all": "Hamısı",
"roster.nodeSelector.tabs.dify": "Dify",
"roster.nodeSelector.tabs.external": "Xarici",
"roster.references.label": "{{name}} agentindən istifadə edən iş axınları",
"roster.references.trigger": "{{name}} agentindən istifadə edən iş axınları",
"roster.saveToRoster": "Agentlər bölməsində yadda saxla",
+15
View File
@@ -144,6 +144,15 @@
"external.references": "Apps, die {{name}} verwenden",
"external.searchPlaceholder": "Agenten suchen…",
"external.updated": "Aktualisiert {{time}}",
"external.workflow.runUnavailable": "Ausführungen externer Agenten sind noch nicht verfügbar.",
"external.workflow.task.characterCount_one": "{{count}} Zeichen",
"external.workflow.task.characterCount_other": "{{count}} Zeichen",
"external.workflow.waitLimit.endWaiting": "Warten beenden",
"external.workflow.waitLimit.help": "Wenden Sie die konfigurierte Fehlerbehandlung an. Die Aufgabe des Agenten kann weiter ausgeführt werden.",
"external.workflow.waitLimit.label": "Wartezeitlimit",
"external.workflow.waitLimit.onTimeout": "Bei Zeitüberschreitung",
"external.workflow.waitLimit.title": "Wartezeitlimit für externen Agenten",
"external.workflow.waitLimit.unit": "Min.",
"roster.createAgent": "Agent erstellen",
"roster.createDialog.description": "Erstellen Sie einen wiederverwendbaren Agent in diesem Workspace.",
"roster.createDialog.title": "Agent erstellen",
@@ -182,8 +191,14 @@
"roster.nodeSelector.createInlineFailed": "Inline-Agent konnte nicht erstellt werden. Bitte aktualisieren und erneut versuchen.",
"roster.nodeSelector.dialogLabel": "Agent auswählen",
"roster.nodeSelector.manageInAgentConsole": "In Agents verwalten",
"roster.nodeSelector.searchAgentsPlaceholder": "Agenten suchen…",
"roster.nodeSelector.searchPlaceholder": "Suchen…",
"roster.nodeSelector.source.dify": "Dify",
"roster.nodeSelector.source.external": "Extern",
"roster.nodeSelector.startFromScratch": "Von Grund auf starten",
"roster.nodeSelector.tabs.all": "Alle",
"roster.nodeSelector.tabs.dify": "Dify",
"roster.nodeSelector.tabs.external": "Extern",
"roster.references.label": "Workflows, die {{name}} verwenden",
"roster.references.trigger": "Workflows, die {{name}} verwenden",
"roster.saveToRoster": "In Agents speichern",
+15
View File
@@ -144,6 +144,15 @@
"external.references": "Apps using {{name}}",
"external.searchPlaceholder": "Search agents…",
"external.updated": "Updated {{time}}",
"external.workflow.runUnavailable": "External agent runs are not available yet.",
"external.workflow.task.characterCount_one": "{{count}} character",
"external.workflow.task.characterCount_other": "{{count}} characters",
"external.workflow.waitLimit.endWaiting": "End waiting",
"external.workflow.waitLimit.help": "Apply the configured error handling. The agent’s task may continue running.",
"external.workflow.waitLimit.label": "Wait limit",
"external.workflow.waitLimit.onTimeout": "On timeout",
"external.workflow.waitLimit.title": "External agent wait limit",
"external.workflow.waitLimit.unit": "min",
"roster.createAgent": "Create agent",
"roster.createDialog.description": "Create a reusable Agent in this workspace.",
"roster.createDialog.title": "Create agent",
@@ -182,8 +191,14 @@
"roster.nodeSelector.createInlineFailed": "Failed to create inline agent. Refresh and try again.",
"roster.nodeSelector.dialogLabel": "Select an agent",
"roster.nodeSelector.manageInAgentConsole": "Manage in Agents",
"roster.nodeSelector.searchAgentsPlaceholder": "Search agents…",
"roster.nodeSelector.searchPlaceholder": "Search…",
"roster.nodeSelector.source.dify": "Dify",
"roster.nodeSelector.source.external": "External",
"roster.nodeSelector.startFromScratch": "Start from Scratch",
"roster.nodeSelector.tabs.all": "All",
"roster.nodeSelector.tabs.dify": "Dify",
"roster.nodeSelector.tabs.external": "External",
"roster.references.label": "Workflows using {{name}}",
"roster.references.trigger": "Workflows using {{name}}",
"roster.saveToRoster": "Save to Agents",
+15
View File
@@ -146,6 +146,15 @@
"external.references": "Aplicaciones que usan {{name}}",
"external.searchPlaceholder": "Buscar agentes…",
"external.updated": "Actualizado {{time}}",
"external.workflow.runUnavailable": "Las ejecuciones de agentes externos aún no están disponibles.",
"external.workflow.task.characterCount_one": "{{count}} carácter",
"external.workflow.task.characterCount_other": "{{count}} caracteres",
"external.workflow.waitLimit.endWaiting": "Finalizar espera",
"external.workflow.waitLimit.help": "Aplica el manejo de errores configurado. La tarea del agente puede seguir ejecutándose.",
"external.workflow.waitLimit.label": "Límite de espera",
"external.workflow.waitLimit.onTimeout": "Al superar el tiempo de espera",
"external.workflow.waitLimit.title": "Límite de espera del agente externo",
"external.workflow.waitLimit.unit": "min",
"roster.createAgent": "Crear agente",
"roster.createDialog.description": "Crea un Agent reutilizable en este espacio de trabajo.",
"roster.createDialog.title": "Crear agente",
@@ -184,8 +193,14 @@
"roster.nodeSelector.createInlineFailed": "Error al crear el agente inline. Actualiza y vuelve a intentarlo.",
"roster.nodeSelector.dialogLabel": "Selecciona un agente",
"roster.nodeSelector.manageInAgentConsole": "Gestionar en Agents",
"roster.nodeSelector.searchAgentsPlaceholder": "Buscar agentes…",
"roster.nodeSelector.searchPlaceholder": "Buscar…",
"roster.nodeSelector.source.dify": "Dify",
"roster.nodeSelector.source.external": "Externo",
"roster.nodeSelector.startFromScratch": "Empezar desde cero",
"roster.nodeSelector.tabs.all": "Todos",
"roster.nodeSelector.tabs.dify": "Dify",
"roster.nodeSelector.tabs.external": "Externo",
"roster.references.label": "Flujos de trabajo que usan {{name}}",
"roster.references.trigger": "Flujos de trabajo que usan {{name}}",
"roster.saveToRoster": "Guardar en Agents",
+15
View File
@@ -144,6 +144,15 @@
"external.references": "برنامه‌های استفاده‌کننده از {{name}}",
"external.searchPlaceholder": "جستجوی عامل‌ها…",
"external.updated": "به‌روزرسانی {{time}}",
"external.workflow.runUnavailable": "اجرای عامل‌های خارجی هنوز در دسترس نیست.",
"external.workflow.task.characterCount_one": "{{count}} نویسه",
"external.workflow.task.characterCount_other": "{{count}} نویسه",
"external.workflow.waitLimit.endWaiting": "پایان انتظار",
"external.workflow.waitLimit.help": "رسیدگی به خطای پیکربندی‌شده را اعمال کنید. ممکن است وظیفهٔ عامل همچنان در حال اجرا باشد.",
"external.workflow.waitLimit.label": "محدودیت انتظار",
"external.workflow.waitLimit.onTimeout": "هنگام پایان مهلت",
"external.workflow.waitLimit.title": "محدودیت زمان انتظار عامل خارجی",
"external.workflow.waitLimit.unit": "دقیقه",
"roster.createAgent": "ایجاد عامل",
"roster.createDialog.description": "یک Agent قابل استفاده مجدد در این فضای کاری ایجاد کنید.",
"roster.createDialog.title": "ایجاد عامل",
@@ -182,8 +191,14 @@
"roster.nodeSelector.createInlineFailed": "ایجاد عامل inline ناموفق بود. تازه‌سازی کنید و دوباره امتحان کنید.",
"roster.nodeSelector.dialogLabel": "یک عامل انتخاب کنید",
"roster.nodeSelector.manageInAgentConsole": "مدیریت در Agents",
"roster.nodeSelector.searchAgentsPlaceholder": "جستجوی عامل‌ها…",
"roster.nodeSelector.searchPlaceholder": "جستجو…",
"roster.nodeSelector.source.dify": "Dify",
"roster.nodeSelector.source.external": "خارجی",
"roster.nodeSelector.startFromScratch": "از صفر شروع کنید",
"roster.nodeSelector.tabs.all": "همه",
"roster.nodeSelector.tabs.dify": "Dify",
"roster.nodeSelector.tabs.external": "خارجی",
"roster.references.label": "گردش‌های کار با استفاده از {{name}}",
"roster.references.trigger": "گردش‌های کار با استفاده از {{name}}",
"roster.saveToRoster": "ذخیره در Agents",
+15
View File
@@ -146,6 +146,15 @@
"external.references": "Applications utilisant {{name}}",
"external.searchPlaceholder": "Rechercher des agents…",
"external.updated": "Mis à jour {{time}}",
"external.workflow.runUnavailable": "Les exécutions d’agents externes ne sont pas encore disponibles.",
"external.workflow.task.characterCount_one": "{{count}} caractère",
"external.workflow.task.characterCount_other": "{{count}} caractères",
"external.workflow.waitLimit.endWaiting": "Arrêter l’attente",
"external.workflow.waitLimit.help": "Appliquez la gestion des erreurs configurée. La tâche de l’agent peut continuer à s’exécuter.",
"external.workflow.waitLimit.label": "Limite d’attente",
"external.workflow.waitLimit.onTimeout": "En cas de délai dépassé",
"external.workflow.waitLimit.title": "Limite d’attente de l’agent externe",
"external.workflow.waitLimit.unit": "min",
"roster.createAgent": "Créer un agent",
"roster.createDialog.description": "Créez un Agent réutilisable dans cet espace de travail.",
"roster.createDialog.title": "Créer un agent",
@@ -184,8 +193,14 @@
"roster.nodeSelector.createInlineFailed": "Échec de la création d’un agent inline. Actualisez et réessayez.",
"roster.nodeSelector.dialogLabel": "Sélectionner un agent",
"roster.nodeSelector.manageInAgentConsole": "Gérer dans Agents",
"roster.nodeSelector.searchAgentsPlaceholder": "Rechercher des agents…",
"roster.nodeSelector.searchPlaceholder": "Rechercher…",
"roster.nodeSelector.source.dify": "Dify",
"roster.nodeSelector.source.external": "Externe",
"roster.nodeSelector.startFromScratch": "Partir de zéro",
"roster.nodeSelector.tabs.all": "Tous",
"roster.nodeSelector.tabs.dify": "Dify",
"roster.nodeSelector.tabs.external": "Externe",
"roster.references.label": "Workflows utilisant {{name}}",
"roster.references.trigger": "Workflows utilisant {{name}}",
"roster.saveToRoster": "Enregistrer dans Agents",
+15
View File
@@ -144,6 +144,15 @@
"external.references": "{{name}} का उपयोग करने वाले ऐप",
"external.searchPlaceholder": "एजेंट खोजें…",
"external.updated": "{{time}} अपडेट किया गया",
"external.workflow.runUnavailable": "बाहरी एजेंट रन अभी उपलब्ध नहीं हैं।",
"external.workflow.task.characterCount_one": "{{count}} वर्ण",
"external.workflow.task.characterCount_other": "{{count}} वर्ण",
"external.workflow.waitLimit.endWaiting": "प्रतीक्षा समाप्त करें",
"external.workflow.waitLimit.help": "कॉन्फ़िगर की गई त्रुटि हैंडलिंग लागू करें। एजेंट का कार्य चलता रह सकता है।",
"external.workflow.waitLimit.label": "प्रतीक्षा सीमा",
"external.workflow.waitLimit.onTimeout": "समय समाप्त होने पर",
"external.workflow.waitLimit.title": "बाहरी एजेंट प्रतीक्षा सीमा",
"external.workflow.waitLimit.unit": "मिनट",
"roster.createAgent": "एजेंट बनाएँ",
"roster.createDialog.description": "इस वर्कस्पेस में एक पुनः उपयोग योग्य Agent बनाएँ।",
"roster.createDialog.title": "एजेंट बनाएँ",
@@ -182,8 +191,14 @@
"roster.nodeSelector.createInlineFailed": "इनलाइन एजेंट बनाने में विफल। रीफ़्रेश करें और पुनः प्रयास करें।",
"roster.nodeSelector.dialogLabel": "एक एजेंट चुनें",
"roster.nodeSelector.manageInAgentConsole": "Agents में प्रबंधित करें",
"roster.nodeSelector.searchAgentsPlaceholder": "एजेंट खोजें…",
"roster.nodeSelector.searchPlaceholder": "खोजें…",
"roster.nodeSelector.source.dify": "Dify",
"roster.nodeSelector.source.external": "बाहरी",
"roster.nodeSelector.startFromScratch": "शुरुआत से आरंभ करें",
"roster.nodeSelector.tabs.all": "सभी",
"roster.nodeSelector.tabs.dify": "Dify",
"roster.nodeSelector.tabs.external": "बाहरी",
"roster.references.label": "{{name}} का उपयोग करने वाले वर्कफ़्लो",
"roster.references.trigger": "{{name}} का उपयोग करने वाले वर्कफ़्लो",
"roster.saveToRoster": "Agents में सहेजें",
+15
View File
@@ -144,6 +144,15 @@
"external.references": "Aplikasi yang menggunakan {{name}}",
"external.searchPlaceholder": "Cari agen…",
"external.updated": "Diperbarui {{time}}",
"external.workflow.runUnavailable": "Eksekusi agen eksternal belum tersedia.",
"external.workflow.task.characterCount_one": "{{count}} karakter",
"external.workflow.task.characterCount_other": "{{count}} karakter",
"external.workflow.waitLimit.endWaiting": "Akhiri penantian",
"external.workflow.waitLimit.help": "Terapkan penanganan error yang dikonfigurasi. Tugas agen mungkin tetap berjalan.",
"external.workflow.waitLimit.label": "Batas tunggu",
"external.workflow.waitLimit.onTimeout": "Saat waktu habis",
"external.workflow.waitLimit.title": "Batas tunggu agen eksternal",
"external.workflow.waitLimit.unit": "mnt",
"roster.createAgent": "Buat agen",
"roster.createDialog.description": "Buat Agent yang dapat digunakan kembali di ruang kerja ini.",
"roster.createDialog.title": "Buat agen",
@@ -182,8 +191,14 @@
"roster.nodeSelector.createInlineFailed": "Gagal membuat agen inline. Segarkan dan coba lagi.",
"roster.nodeSelector.dialogLabel": "Pilih agen",
"roster.nodeSelector.manageInAgentConsole": "Kelola di Agents",
"roster.nodeSelector.searchAgentsPlaceholder": "Cari agen…",
"roster.nodeSelector.searchPlaceholder": "Cari…",
"roster.nodeSelector.source.dify": "Dify",
"roster.nodeSelector.source.external": "Eksternal",
"roster.nodeSelector.startFromScratch": "Mulai dari Awal",
"roster.nodeSelector.tabs.all": "Semua",
"roster.nodeSelector.tabs.dify": "Dify",
"roster.nodeSelector.tabs.external": "Eksternal",
"roster.references.label": "Alur kerja yang menggunakan {{name}}",
"roster.references.trigger": "Alur kerja yang menggunakan {{name}}",
"roster.saveToRoster": "Simpan ke Agents",
+15
View File
@@ -146,6 +146,15 @@
"external.references": "App che utilizzano {{name}}",
"external.searchPlaceholder": "Cerca agenti…",
"external.updated": "Aggiornato {{time}}",
"external.workflow.runUnavailable": "Le esecuzioni degli agenti esterni non sono ancora disponibili.",
"external.workflow.task.characterCount_one": "{{count}} carattere",
"external.workflow.task.characterCount_other": "{{count}} caratteri",
"external.workflow.waitLimit.endWaiting": "Termina l’attesa",
"external.workflow.waitLimit.help": "Applica la gestione degli errori configurata. L’attività dell’agente potrebbe continuare a essere eseguita.",
"external.workflow.waitLimit.label": "Limite di attesa",
"external.workflow.waitLimit.onTimeout": "Alla scadenza",
"external.workflow.waitLimit.title": "Limite di attesa dell’agente esterno",
"external.workflow.waitLimit.unit": "min",
"roster.createAgent": "Crea agente",
"roster.createDialog.description": "Crea un Agent riutilizzabile in questo workspace.",
"roster.createDialog.title": "Crea agente",
@@ -184,8 +193,14 @@
"roster.nodeSelector.createInlineFailed": "Impossibile creare l’agente inline. Aggiorna e riprova.",
"roster.nodeSelector.dialogLabel": "Seleziona un agente",
"roster.nodeSelector.manageInAgentConsole": "Gestisci in Agents",
"roster.nodeSelector.searchAgentsPlaceholder": "Cerca agenti…",
"roster.nodeSelector.searchPlaceholder": "Cerca…",
"roster.nodeSelector.source.dify": "Dify",
"roster.nodeSelector.source.external": "Esterno",
"roster.nodeSelector.startFromScratch": "Inizia da zero",
"roster.nodeSelector.tabs.all": "Tutte",
"roster.nodeSelector.tabs.dify": "Dify",
"roster.nodeSelector.tabs.external": "Esterno",
"roster.references.label": "Workflow che usano {{name}}",
"roster.references.trigger": "Workflow che usano {{name}}",
"roster.saveToRoster": "Salva in Agents",
+15
View File
@@ -144,6 +144,15 @@
"external.references": "{{name}} を使用するアプリ",
"external.searchPlaceholder": "エージェントを検索…",
"external.updated": "更新:{{time}}",
"external.workflow.runUnavailable": "外部エージェントの実行はまだ利用できません。",
"external.workflow.task.characterCount_one": "{{count}} 文字",
"external.workflow.task.characterCount_other": "{{count}} 文字",
"external.workflow.waitLimit.endWaiting": "待機を終了",
"external.workflow.waitLimit.help": "設定されたエラー処理を適用します。エージェントのタスクは引き続き実行される場合があります。",
"external.workflow.waitLimit.label": "待機上限",
"external.workflow.waitLimit.onTimeout": "タイムアウト時",
"external.workflow.waitLimit.title": "外部エージェントの待機上限",
"external.workflow.waitLimit.unit": "分",
"roster.createAgent": "エージェントを作成",
"roster.createDialog.description": "このワークスペースに再利用可能な Agent を作成します。",
"roster.createDialog.title": "エージェントを作成",
@@ -182,8 +191,14 @@
"roster.nodeSelector.createInlineFailed": "インラインエージェントの作成に失敗しました。更新してもう一度お試しください。",
"roster.nodeSelector.dialogLabel": "エージェントを選択",
"roster.nodeSelector.manageInAgentConsole": "Agents で管理",
"roster.nodeSelector.searchAgentsPlaceholder": "エージェントを検索…",
"roster.nodeSelector.searchPlaceholder": "検索…",
"roster.nodeSelector.source.dify": "Dify",
"roster.nodeSelector.source.external": "外部",
"roster.nodeSelector.startFromScratch": "ゼロから始める",
"roster.nodeSelector.tabs.all": "すべて",
"roster.nodeSelector.tabs.dify": "Dify",
"roster.nodeSelector.tabs.external": "外部",
"roster.references.label": "{{name}} を使用するワークフロー",
"roster.references.trigger": "{{name}} を使用するワークフロー",
"roster.saveToRoster": "Agents に保存",
+15
View File
@@ -144,6 +144,15 @@
"external.references": "{{name}}을(를) 사용하는 앱",
"external.searchPlaceholder": "에이전트 검색…",
"external.updated": "{{time}} 업데이트됨",
"external.workflow.runUnavailable": "외부 에이전트 실행은 아직 사용할 수 없습니다.",
"external.workflow.task.characterCount_one": "{{count}}자",
"external.workflow.task.characterCount_other": "{{count}}자",
"external.workflow.waitLimit.endWaiting": "대기 종료",
"external.workflow.waitLimit.help": "구성된 오류 처리를 적용합니다. 에이전트 작업은 계속 실행될 수 있습니다.",
"external.workflow.waitLimit.label": "대기 제한",
"external.workflow.waitLimit.onTimeout": "시간 초과 시",
"external.workflow.waitLimit.title": "외부 에이전트 대기 제한",
"external.workflow.waitLimit.unit": "분",
"roster.createAgent": "에이전트 만들기",
"roster.createDialog.description": "이 워크스페이스에 재사용 가능한 Agent 를 만듭니다.",
"roster.createDialog.title": "에이전트 만들기",
@@ -182,8 +191,14 @@
"roster.nodeSelector.createInlineFailed": "인라인 에이전트 생성에 실패했습니다. 새로 고침 후 다시 시도하세요.",
"roster.nodeSelector.dialogLabel": "에이전트 선택",
"roster.nodeSelector.manageInAgentConsole": "Agents에서 관리",
"roster.nodeSelector.searchAgentsPlaceholder": "에이전트 검색…",
"roster.nodeSelector.searchPlaceholder": "검색…",
"roster.nodeSelector.source.dify": "Dify",
"roster.nodeSelector.source.external": "외부",
"roster.nodeSelector.startFromScratch": "처음부터 시작",
"roster.nodeSelector.tabs.all": "전체",
"roster.nodeSelector.tabs.dify": "Dify",
"roster.nodeSelector.tabs.external": "외부",
"roster.references.label": "{{name}}을(를) 사용하는 워크플로",
"roster.references.trigger": "{{name}}을(를) 사용하는 워크플로",
"roster.saveToRoster": "Agents에 저장",
+15
View File
@@ -144,6 +144,15 @@
"external.references": "ແອັບທີ່ໃຊ້ {{name}}",
"external.searchPlaceholder": "ຄົ້ນຫາຕົວແທນ…",
"external.updated": "ອັບເດດ {{time}}",
"external.workflow.runUnavailable": "ການເຮັດວຽກຂອງຕົວແທນພາຍນອກຍັງບໍ່ພ້ອມໃຊ້.",
"external.workflow.task.characterCount_one": "{{count}} ຕົວອັກສອນ",
"external.workflow.task.characterCount_other": "{{count}} ຕົວອັກສອນ",
"external.workflow.waitLimit.endWaiting": "ຢຸດລໍຖ້າ",
"external.workflow.waitLimit.help": "ນຳໃຊ້ການຈັດການຂໍ້ຜິດພາດທີ່ຕັ້ງຄ່າໄວ້. ວຽກຂອງຕົວແທນອາດຍັງດຳເນີນຕໍ່.",
"external.workflow.waitLimit.label": "ຂີດຈຳກັດການລໍຖ້າ",
"external.workflow.waitLimit.onTimeout": "ເມື່ອໝົດເວລາ",
"external.workflow.waitLimit.title": "ຂີດຈຳກັດການລໍຖ້າຂອງຕົວແທນພາຍນອກ",
"external.workflow.waitLimit.unit": "ນາທີ",
"roster.createAgent": "ສ້າງຕົວແທນ",
"roster.createDialog.description": "ສ້າງຕົວແທນທີ່ສາມາດນຳມາໃຊ້ໃໝ່ໄດ້ໃນພື້ນທີ່ເຮັດວຽກນີ້.",
"roster.createDialog.title": "ສ້າງຕົວແທນ",
@@ -182,8 +191,14 @@
"roster.nodeSelector.createInlineFailed": "ສ້າງຕົວແທນ inline ບໍ່ສຳເລັດ. ກະລຸນາໂຫຼດໜ້າຄືນໃໝ່ ແລະ ລອງອີກຄັ້ງ.",
"roster.nodeSelector.dialogLabel": "ເລືອກຕົວແທນ",
"roster.nodeSelector.manageInAgentConsole": "ຈັດການໃນ Agents",
"roster.nodeSelector.searchAgentsPlaceholder": "ຄົ້ນຫາຕົວແທນ…",
"roster.nodeSelector.searchPlaceholder": "ຄົ້ນຫາ...",
"roster.nodeSelector.source.dify": "Dify",
"roster.nodeSelector.source.external": "ພາຍນອກ",
"roster.nodeSelector.startFromScratch": "ເລີ່ມຈາກສູນ",
"roster.nodeSelector.tabs.all": "ທັງໝົດ",
"roster.nodeSelector.tabs.dify": "Dify",
"roster.nodeSelector.tabs.external": "ພາຍນອກ",
"roster.references.label": "ເວີກໂຟລທີ່ໃຊ້ {{name}}",
"roster.references.trigger": "ເວີກໂຟລທີ່ໃຊ້ {{name}}",
"roster.saveToRoster": "ບັນທຶກລົງໃນ Agents",
+15
View File
@@ -144,6 +144,15 @@
"external.references": "Apps die {{name}} gebruiken",
"external.searchPlaceholder": "Agenten zoeken…",
"external.updated": "Bijgewerkt {{time}}",
"external.workflow.runUnavailable": "Uitvoeringen van externe agents zijn nog niet beschikbaar.",
"external.workflow.task.characterCount_one": "{{count}} teken",
"external.workflow.task.characterCount_other": "{{count}} tekens",
"external.workflow.waitLimit.endWaiting": "Wachten beëindigen",
"external.workflow.waitLimit.help": "Pas de geconfigureerde foutafhandeling toe. De taak van de agent kan blijven uitvoeren.",
"external.workflow.waitLimit.label": "Wachtlimiet",
"external.workflow.waitLimit.onTimeout": "Bij time-out",
"external.workflow.waitLimit.title": "Wachtlimiet voor externe agent",
"external.workflow.waitLimit.unit": "min",
"roster.createAgent": "Agent aanmaken",
"roster.createDialog.description": "Maak een herbruikbare Agent in deze workspace.",
"roster.createDialog.title": "Agent aanmaken",
@@ -182,8 +191,14 @@
"roster.nodeSelector.createInlineFailed": "Inline-agent aanmaken mislukt. Vernieuw en probeer het opnieuw.",
"roster.nodeSelector.dialogLabel": "Selecteer een agent",
"roster.nodeSelector.manageInAgentConsole": "Beheren in Agents",
"roster.nodeSelector.searchAgentsPlaceholder": "Agents zoeken…",
"roster.nodeSelector.searchPlaceholder": "Zoeken…",
"roster.nodeSelector.source.dify": "Dify",
"roster.nodeSelector.source.external": "Extern",
"roster.nodeSelector.startFromScratch": "Vanaf nul beginnen",
"roster.nodeSelector.tabs.all": "Alle",
"roster.nodeSelector.tabs.dify": "Dify",
"roster.nodeSelector.tabs.external": "Extern",
"roster.references.label": "Workflows die {{name}} gebruiken",
"roster.references.trigger": "Workflows die {{name}} gebruiken",
"roster.saveToRoster": "Opslaan in Agents",
+15
View File
@@ -148,6 +148,15 @@
"external.references": "Aplikacje korzystające z {{name}}",
"external.searchPlaceholder": "Szukaj agentów…",
"external.updated": "Zaktualizowano {{time}}",
"external.workflow.runUnavailable": "Uruchomienia agentów zewnętrznych nie są jeszcze dostępne.",
"external.workflow.task.characterCount_one": "{{count}} znak",
"external.workflow.task.characterCount_other": "{{count}} znaków",
"external.workflow.waitLimit.endWaiting": "Zakończ oczekiwanie",
"external.workflow.waitLimit.help": "Zastosuj skonfigurowaną obsługę błędów. Zadanie agenta może nadal działać.",
"external.workflow.waitLimit.label": "Limit oczekiwania",
"external.workflow.waitLimit.onTimeout": "Po przekroczeniu limitu czasu",
"external.workflow.waitLimit.title": "Limit oczekiwania agenta zewnętrznego",
"external.workflow.waitLimit.unit": "min",
"roster.createAgent": "Utwórz agenta",
"roster.createDialog.description": "Utwórz wielokrotnego użytku Agent w tym workspace.",
"roster.createDialog.title": "Utwórz agenta",
@@ -186,8 +195,14 @@
"roster.nodeSelector.createInlineFailed": "Nie udało się utworzyć inline agenta. Odśwież i spróbuj ponownie.",
"roster.nodeSelector.dialogLabel": "Wybierz agenta",
"roster.nodeSelector.manageInAgentConsole": "Zarządzaj w Agents",
"roster.nodeSelector.searchAgentsPlaceholder": "Wyszukaj agentów…",
"roster.nodeSelector.searchPlaceholder": "Szukaj…",
"roster.nodeSelector.source.dify": "Dify",
"roster.nodeSelector.source.external": "Zewnętrzne",
"roster.nodeSelector.startFromScratch": "Zacznij od zera",
"roster.nodeSelector.tabs.all": "Wszystkie",
"roster.nodeSelector.tabs.dify": "Dify",
"roster.nodeSelector.tabs.external": "Zewnętrzne",
"roster.references.label": "Workflow używające {{name}}",
"roster.references.trigger": "Workflow używające {{name}}",
"roster.saveToRoster": "Zapisz w Agents",
+15
View File
@@ -146,6 +146,15 @@
"external.references": "Aplicativos que usam {{name}}",
"external.searchPlaceholder": "Pesquisar agentes…",
"external.updated": "Atualizado {{time}}",
"external.workflow.runUnavailable": "Execuções de agentes externos ainda não estão disponíveis.",
"external.workflow.task.characterCount_one": "{{count}} caractere",
"external.workflow.task.characterCount_other": "{{count}} caracteres",
"external.workflow.waitLimit.endWaiting": "Encerrar espera",
"external.workflow.waitLimit.help": "Aplique o tratamento de erros configurado. A tarefa do agente pode continuar em execução.",
"external.workflow.waitLimit.label": "Limite de espera",
"external.workflow.waitLimit.onTimeout": "Ao atingir o tempo limite",
"external.workflow.waitLimit.title": "Limite de espera do agente externo",
"external.workflow.waitLimit.unit": "min",
"roster.createAgent": "Criar agente",
"roster.createDialog.description": "Crie um Agent reutilizável neste workspace.",
"roster.createDialog.title": "Criar agente",
@@ -184,8 +193,14 @@
"roster.nodeSelector.createInlineFailed": "Falha ao criar agente inline. Atualize e tente novamente.",
"roster.nodeSelector.dialogLabel": "Selecione um agente",
"roster.nodeSelector.manageInAgentConsole": "Gerenciar no Agents",
"roster.nodeSelector.searchAgentsPlaceholder": "Pesquisar agentes…",
"roster.nodeSelector.searchPlaceholder": "Pesquisar…",
"roster.nodeSelector.source.dify": "Dify",
"roster.nodeSelector.source.external": "Externo",
"roster.nodeSelector.startFromScratch": "Começar do zero",
"roster.nodeSelector.tabs.all": "Todos",
"roster.nodeSelector.tabs.dify": "Dify",
"roster.nodeSelector.tabs.external": "Externo",
"roster.references.label": "Workflows que usam {{name}}",
"roster.references.trigger": "Workflows que usam {{name}}",
"roster.saveToRoster": "Salvar no Agents",
+15
View File
@@ -146,6 +146,15 @@
"external.references": "Aplicații care folosesc {{name}}",
"external.searchPlaceholder": "Caută agenți…",
"external.updated": "Actualizat {{time}}",
"external.workflow.runUnavailable": "Rulările agenților externi nu sunt încă disponibile.",
"external.workflow.task.characterCount_one": "{{count}} caracter",
"external.workflow.task.characterCount_other": "{{count}} caractere",
"external.workflow.waitLimit.endWaiting": "Oprește așteptarea",
"external.workflow.waitLimit.help": "Aplicați gestionarea configurată a erorilor. Sarcina agentului poate continua să ruleze.",
"external.workflow.waitLimit.label": "Limită de așteptare",
"external.workflow.waitLimit.onTimeout": "La expirarea timpului",
"external.workflow.waitLimit.title": "Limită de așteptare pentru agentul extern",
"external.workflow.waitLimit.unit": "min",
"roster.createAgent": "Creează agent",
"roster.createDialog.description": "Creați un Agent reutilizabil în acest workspace.",
"roster.createDialog.title": "Creează agent",
@@ -184,8 +193,14 @@
"roster.nodeSelector.createInlineFailed": "Crearea agentului inline a eșuat. Reîmprospătați și încercați din nou.",
"roster.nodeSelector.dialogLabel": "Selectați un agent",
"roster.nodeSelector.manageInAgentConsole": "Gestionați în Agents",
"roster.nodeSelector.searchAgentsPlaceholder": "Căutați agenți…",
"roster.nodeSelector.searchPlaceholder": "Căutați…",
"roster.nodeSelector.source.dify": "Dify",
"roster.nodeSelector.source.external": "Extern",
"roster.nodeSelector.startFromScratch": "Începeți de la zero",
"roster.nodeSelector.tabs.all": "Toate",
"roster.nodeSelector.tabs.dify": "Dify",
"roster.nodeSelector.tabs.external": "Extern",
"roster.references.label": "Workflow-uri care folosesc {{name}}",
"roster.references.trigger": "Workflow-uri care folosesc {{name}}",
"roster.saveToRoster": "Salvează în Agents",
+15
View File
@@ -148,6 +148,15 @@
"external.references": "Приложения, использующие {{name}}",
"external.searchPlaceholder": "Поиск агентов…",
"external.updated": "Обновлено {{time}}",
"external.workflow.runUnavailable": "Запуски внешних агентов пока недоступны.",
"external.workflow.task.characterCount_one": "{{count}} символ",
"external.workflow.task.characterCount_other": "{{count}} символов",
"external.workflow.waitLimit.endWaiting": "Завершить ожидание",
"external.workflow.waitLimit.help": "Примените настроенную обработку ошибок. Задача агента может продолжать выполняться.",
"external.workflow.waitLimit.label": "Лимит ожидания",
"external.workflow.waitLimit.onTimeout": "При превышении времени ожидания",
"external.workflow.waitLimit.title": "Лимит ожидания внешнего агента",
"external.workflow.waitLimit.unit": "мин.",
"roster.createAgent": "Создать агента",
"roster.createDialog.description": "Создайте переиспользуемый Agent в этом workspace.",
"roster.createDialog.title": "Создать агента",
@@ -186,8 +195,14 @@
"roster.nodeSelector.createInlineFailed": "Не удалось создать встроенного агента. Обновите и попробуйте снова.",
"roster.nodeSelector.dialogLabel": "Выберите агента",
"roster.nodeSelector.manageInAgentConsole": "Управлять в Agents",
"roster.nodeSelector.searchAgentsPlaceholder": "Поиск агентов…",
"roster.nodeSelector.searchPlaceholder": "Поиск…",
"roster.nodeSelector.source.dify": "Dify",
"roster.nodeSelector.source.external": "Внешние",
"roster.nodeSelector.startFromScratch": "Начать с нуля",
"roster.nodeSelector.tabs.all": "Все",
"roster.nodeSelector.tabs.dify": "Dify",
"roster.nodeSelector.tabs.external": "Внешние",
"roster.references.label": "Рабочие процессы, использующие {{name}}",
"roster.references.trigger": "Рабочие процессы, использующие {{name}}",
"roster.saveToRoster": "Сохранить в Agents",
+15
View File
@@ -148,6 +148,15 @@
"external.references": "Aplikacije, ki uporabljajo {{name}}",
"external.searchPlaceholder": "Iskanje agentov…",
"external.updated": "Posodobljeno {{time}}",
"external.workflow.runUnavailable": "Izvajanja zunanjih agentov še niso na voljo.",
"external.workflow.task.characterCount_one": "{{count}} znak",
"external.workflow.task.characterCount_other": "{{count}} znakov",
"external.workflow.waitLimit.endWaiting": "Končaj čakanje",
"external.workflow.waitLimit.help": "Uporabite konfigurirano obravnavo napak. Naloga agenta se lahko še naprej izvaja.",
"external.workflow.waitLimit.label": "Omejitev čakanja",
"external.workflow.waitLimit.onTimeout": "Ob prekoračitvi časa",
"external.workflow.waitLimit.title": "Omejitev čakanja zunanjega agenta",
"external.workflow.waitLimit.unit": "min",
"roster.createAgent": "Ustvari agenta",
"roster.createDialog.description": "Ustvarite ponovno uporabnega Agent v tem workspace.",
"roster.createDialog.title": "Ustvari agenta",
@@ -186,8 +195,14 @@
"roster.nodeSelector.createInlineFailed": "Vgrajenega agenta ni bilo mogoče ustvariti. Osvežite in poskusite znova.",
"roster.nodeSelector.dialogLabel": "Izberite agenta",
"roster.nodeSelector.manageInAgentConsole": "Upravljaj v Agents",
"roster.nodeSelector.searchAgentsPlaceholder": "Iskanje agentov…",
"roster.nodeSelector.searchPlaceholder": "Išči…",
"roster.nodeSelector.source.dify": "Dify",
"roster.nodeSelector.source.external": "Zunanji",
"roster.nodeSelector.startFromScratch": "Začni iz nič",
"roster.nodeSelector.tabs.all": "Vsi",
"roster.nodeSelector.tabs.dify": "Dify",
"roster.nodeSelector.tabs.external": "Zunanji",
"roster.references.label": "Poteki dela, ki uporabljajo {{name}}",
"roster.references.trigger": "Poteki dela, ki uporabljajo {{name}}",
"roster.saveToRoster": "Shrani v Agents",
+15
View File
@@ -144,6 +144,15 @@
"external.references": "แอปที่ใช้ {{name}}",
"external.searchPlaceholder": "ค้นหาตัวแทน…",
"external.updated": "อัปเดต{{time}}",
"external.workflow.runUnavailable": "การเรียกใช้เอเจนต์ภายนอกยังไม่พร้อมใช้งาน",
"external.workflow.task.characterCount_one": "{{count}} อักขระ",
"external.workflow.task.characterCount_other": "{{count}} อักขระ",
"external.workflow.waitLimit.endWaiting": "สิ้นสุดการรอ",
"external.workflow.waitLimit.help": "ใช้การจัดการข้อผิดพลาดที่กำหนดค่าไว้ งานของเอเจนต์อาจยังทำงานต่อไป",
"external.workflow.waitLimit.label": "ขีดจำกัดเวลารอ",
"external.workflow.waitLimit.onTimeout": "เมื่อหมดเวลา",
"external.workflow.waitLimit.title": "ขีดจำกัดเวลารอของเอเจนต์ภายนอก",
"external.workflow.waitLimit.unit": "นาที",
"roster.createAgent": "สร้างตัวแทน",
"roster.createDialog.description": "สร้าง Agent ที่นำกลับมาใช้ใหม่ได้ในเวิร์กสเปซนี้",
"roster.createDialog.title": "สร้างตัวแทน",
@@ -182,8 +191,14 @@
"roster.nodeSelector.createInlineFailed": "สร้างตัวแทนแบบอินไลน์ไม่สำเร็จ รีเฟรชและลองอีกครั้ง",
"roster.nodeSelector.dialogLabel": "เลือกตัวแทน",
"roster.nodeSelector.manageInAgentConsole": "จัดการใน Agents",
"roster.nodeSelector.searchAgentsPlaceholder": "ค้นหาตัวแทน…",
"roster.nodeSelector.searchPlaceholder": "ค้นหา…",
"roster.nodeSelector.source.dify": "Dify",
"roster.nodeSelector.source.external": "ภายนอก",
"roster.nodeSelector.startFromScratch": "เริ่มต้นจากศูนย์",
"roster.nodeSelector.tabs.all": "ทั้งหมด",
"roster.nodeSelector.tabs.dify": "Dify",
"roster.nodeSelector.tabs.external": "ภายนอก",
"roster.references.label": "เวิร์กโฟลว์ที่ใช้ {{name}}",
"roster.references.trigger": "เวิร์กโฟลว์ที่ใช้ {{name}}",
"roster.saveToRoster": "บันทึกไปยัง Agents",
+15
View File
@@ -144,6 +144,15 @@
"external.references": "{{name}} kullanan uygulamalar",
"external.searchPlaceholder": "Ajan ara…",
"external.updated": "Güncellendi: {{time}}",
"external.workflow.runUnavailable": "Harici ajan çalıştırmaları henüz kullanılamıyor.",
"external.workflow.task.characterCount_one": "{{count}} karakter",
"external.workflow.task.characterCount_other": "{{count}} karakter",
"external.workflow.waitLimit.endWaiting": "Beklemeyi sonlandır",
"external.workflow.waitLimit.help": "Yapılandırılmış hata işlemesini uygulayın. Ajanın görevi çalışmaya devam edebilir.",
"external.workflow.waitLimit.label": "Bekleme sınırı",
"external.workflow.waitLimit.onTimeout": "Zaman aşımında",
"external.workflow.waitLimit.title": "Harici ajan bekleme sınırı",
"external.workflow.waitLimit.unit": "dk",
"roster.createAgent": "Ajan oluştur",
"roster.createDialog.description": "Bu workspace içinde yeniden kullanılabilir bir Agent oluşturun.",
"roster.createDialog.title": "Ajan oluştur",
@@ -182,8 +191,14 @@
"roster.nodeSelector.createInlineFailed": "Inline ajan oluşturulamadı. Yenileyip tekrar deneyin.",
"roster.nodeSelector.dialogLabel": "Bir ajan seç",
"roster.nodeSelector.manageInAgentConsole": "Agents'da yönet",
"roster.nodeSelector.searchAgentsPlaceholder": "Ajan ara…",
"roster.nodeSelector.searchPlaceholder": "Ara…",
"roster.nodeSelector.source.dify": "Dify",
"roster.nodeSelector.source.external": "Harici",
"roster.nodeSelector.startFromScratch": "Sıfırdan Başla",
"roster.nodeSelector.tabs.all": "Tümü",
"roster.nodeSelector.tabs.dify": "Dify",
"roster.nodeSelector.tabs.external": "Harici",
"roster.references.label": "{{name}} kullanan iş akışları",
"roster.references.trigger": "{{name}} kullanan iş akışları",
"roster.saveToRoster": "Agents'a kaydet",
+15
View File
@@ -148,6 +148,15 @@
"external.references": "Застосунки, що використовують {{name}}",
"external.searchPlaceholder": "Пошук агентів…",
"external.updated": "Оновлено {{time}}",
"external.workflow.runUnavailable": "Запуски зовнішніх агентів ще недоступні.",
"external.workflow.task.characterCount_one": "{{count}} символ",
"external.workflow.task.characterCount_other": "{{count}} символів",
"external.workflow.waitLimit.endWaiting": "Завершити очікування",
"external.workflow.waitLimit.help": "Застосуйте налаштовану обробку помилок. Завдання агента може продовжувати виконуватися.",
"external.workflow.waitLimit.label": "Ліміт очікування",
"external.workflow.waitLimit.onTimeout": "У разі перевищення часу очікування",
"external.workflow.waitLimit.title": "Ліміт очікування зовнішнього агента",
"external.workflow.waitLimit.unit": "хв",
"roster.createAgent": "Створити агента",
"roster.createDialog.description": "Створіть перевикористовний Agent у цьому workspace.",
"roster.createDialog.title": "Створити агента",
@@ -186,8 +195,14 @@
"roster.nodeSelector.createInlineFailed": "Не вдалося створити вбудованого агента. Оновіть і спробуйте ще раз.",
"roster.nodeSelector.dialogLabel": "Виберіть агента",
"roster.nodeSelector.manageInAgentConsole": "Керувати в Agents",
"roster.nodeSelector.searchAgentsPlaceholder": "Пошук агентів…",
"roster.nodeSelector.searchPlaceholder": "Пошук…",
"roster.nodeSelector.source.dify": "Dify",
"roster.nodeSelector.source.external": "Зовнішні",
"roster.nodeSelector.startFromScratch": "Почати з нуля",
"roster.nodeSelector.tabs.all": "Усі",
"roster.nodeSelector.tabs.dify": "Dify",
"roster.nodeSelector.tabs.external": "Зовнішні",
"roster.references.label": "Робочі процеси, що використовують {{name}}",
"roster.references.trigger": "Робочі процеси, що використовують {{name}}",
"roster.saveToRoster": "Зберегти в Agents",
+15
View File
@@ -144,6 +144,15 @@
"external.references": "Ứng dụng sử dụng {{name}}",
"external.searchPlaceholder": "Tìm tác nhân…",
"external.updated": "Đã cập nhật {{time}}",
"external.workflow.runUnavailable": "Các lượt chạy tác nhân bên ngoài hiện chưa khả dụng.",
"external.workflow.task.characterCount_one": "{{count}} ký tự",
"external.workflow.task.characterCount_other": "{{count}} ký tự",
"external.workflow.waitLimit.endWaiting": "Kết thúc chờ",
"external.workflow.waitLimit.help": "Áp dụng cách xử lý lỗi đã cấu hình. Tác vụ của tác nhân có thể vẫn tiếp tục chạy.",
"external.workflow.waitLimit.label": "Giới hạn chờ",
"external.workflow.waitLimit.onTimeout": "Khi hết thời gian chờ",
"external.workflow.waitLimit.title": "Giới hạn chờ của tác nhân bên ngoài",
"external.workflow.waitLimit.unit": "phút",
"roster.createAgent": "Tạo tác nhân",
"roster.createDialog.description": "Tạo một Agent có thể tái sử dụng trong workspace này.",
"roster.createDialog.title": "Tạo tác nhân",
@@ -182,8 +191,14 @@
"roster.nodeSelector.createInlineFailed": "Tạo tác nhân inline thất bại. Làm mới và thử lại.",
"roster.nodeSelector.dialogLabel": "Chọn một tác nhân",
"roster.nodeSelector.manageInAgentConsole": "Quản lý trong Agents",
"roster.nodeSelector.searchAgentsPlaceholder": "Tìm kiếm tác nhân…",
"roster.nodeSelector.searchPlaceholder": "Tìm kiếm…",
"roster.nodeSelector.source.dify": "Dify",
"roster.nodeSelector.source.external": "Bên ngoài",
"roster.nodeSelector.startFromScratch": "Bắt đầu từ đầu",
"roster.nodeSelector.tabs.all": "Tất cả",
"roster.nodeSelector.tabs.dify": "Dify",
"roster.nodeSelector.tabs.external": "Bên ngoài",
"roster.references.label": "Quy trình làm việc đang dùng {{name}}",
"roster.references.trigger": "Quy trình làm việc đang dùng {{name}}",
"roster.saveToRoster": "Lưu vào Agents",
@@ -144,6 +144,15 @@
"external.references": "使用 {{name}} 的应用",
"external.searchPlaceholder": "搜索 Agent…",
"external.updated": "更新于 {{time}}",
"external.workflow.runUnavailable": "外部 Agent 运行暂不可用。",
"external.workflow.task.characterCount_one": "{{count}} 个字符",
"external.workflow.task.characterCount_other": "{{count}} 个字符",
"external.workflow.waitLimit.endWaiting": "结束等待",
"external.workflow.waitLimit.help": "应用已配置的错误处理。Agent 的任务可能仍会继续运行。",
"external.workflow.waitLimit.label": "等待时限",
"external.workflow.waitLimit.onTimeout": "超时时",
"external.workflow.waitLimit.title": "外部 Agent 等待时限",
"external.workflow.waitLimit.unit": "分钟",
"roster.createAgent": "创建 Agent",
"roster.createDialog.description": "在当前工作区创建一个可复用 Agent。",
"roster.createDialog.title": "创建 Agent",
@@ -182,8 +191,14 @@
"roster.nodeSelector.createInlineFailed": "Inline Agent 创建失败。请刷新后重试。",
"roster.nodeSelector.dialogLabel": "选择 Agent",
"roster.nodeSelector.manageInAgentConsole": "在 Agents 中管理",
"roster.nodeSelector.searchAgentsPlaceholder": "搜索 Agent…",
"roster.nodeSelector.searchPlaceholder": "搜索…",
"roster.nodeSelector.source.dify": "Dify",
"roster.nodeSelector.source.external": "外部",
"roster.nodeSelector.startFromScratch": "从空白开始",
"roster.nodeSelector.tabs.all": "全部",
"roster.nodeSelector.tabs.dify": "Dify",
"roster.nodeSelector.tabs.external": "外部",
"roster.references.label": "使用 {{name}} 的工作流",
"roster.references.trigger": "使用 {{name}} 的工作流",
"roster.saveToRoster": "保存到 Agents",
@@ -144,6 +144,15 @@
"external.references": "使用 {{name}} 的應用程式",
"external.searchPlaceholder": "搜尋 Agent…",
"external.updated": "更新於 {{time}}",
"external.workflow.runUnavailable": "外部 Agent 執行暫不可用。",
"external.workflow.task.characterCount_one": "{{count}} 個字元",
"external.workflow.task.characterCount_other": "{{count}} 個字元",
"external.workflow.waitLimit.endWaiting": "結束等待",
"external.workflow.waitLimit.help": "套用已設定的錯誤處理。Agent 的工作可能仍會繼續執行。",
"external.workflow.waitLimit.label": "等待時限",
"external.workflow.waitLimit.onTimeout": "逾時時",
"external.workflow.waitLimit.title": "外部 Agent 等待時限",
"external.workflow.waitLimit.unit": "分鐘",
"roster.createAgent": "建立 Agent",
"roster.createDialog.description": "在目前工作區建立一個可複用 Agent。",
"roster.createDialog.title": "建立 Agent",
@@ -182,8 +191,14 @@
"roster.nodeSelector.createInlineFailed": "Inline Agent 建立失敗。請重新整理後再試。",
"roster.nodeSelector.dialogLabel": "選擇 Agent",
"roster.nodeSelector.manageInAgentConsole": "在 Agents 中管理",
"roster.nodeSelector.searchAgentsPlaceholder": "搜尋 Agent…",
"roster.nodeSelector.searchPlaceholder": "搜尋…",
"roster.nodeSelector.source.dify": "Dify",
"roster.nodeSelector.source.external": "外部",
"roster.nodeSelector.startFromScratch": "從空白開始",
"roster.nodeSelector.tabs.all": "全部",
"roster.nodeSelector.tabs.dify": "Dify",
"roster.nodeSelector.tabs.external": "外部",
"roster.references.label": "使用 {{name}} 的工作流程",
"roster.references.trigger": "使用 {{name}} 的工作流程",
"roster.saveToRoster": "儲存到 Agents",
+14
View File
@@ -321,6 +321,14 @@ export default interface Resources {
'external.references': 'Apps using {{name}}'
'external.searchPlaceholder': 'Search agents…'
'external.updated': 'Updated {{time}}'
'external.workflow.runUnavailable': 'External agent runs are not available yet.'
'external.workflow.task.characterCount': '{{count}} character' | '{{count}} characters'
'external.workflow.waitLimit.endWaiting': 'End waiting'
'external.workflow.waitLimit.help': 'Apply the configured error handling. The agent’s task may continue running.'
'external.workflow.waitLimit.label': 'Wait limit'
'external.workflow.waitLimit.onTimeout': 'On timeout'
'external.workflow.waitLimit.title': 'External agent wait limit'
'external.workflow.waitLimit.unit': 'min'
'roster.createAgent': 'Create agent'
'roster.createDialog.description': 'Create a reusable Agent in this workspace.'
'roster.createDialog.title': 'Create agent'
@@ -359,8 +367,14 @@ export default interface Resources {
'roster.nodeSelector.createInlineFailed': 'Failed to create inline agent. Refresh and try again.'
'roster.nodeSelector.dialogLabel': 'Select an agent'
'roster.nodeSelector.manageInAgentConsole': 'Manage in Agents'
'roster.nodeSelector.searchAgentsPlaceholder': 'Search agents…'
'roster.nodeSelector.searchPlaceholder': 'Search…'
'roster.nodeSelector.source.dify': 'Dify'
'roster.nodeSelector.source.external': 'External'
'roster.nodeSelector.startFromScratch': 'Start from Scratch'
'roster.nodeSelector.tabs.all': 'All'
'roster.nodeSelector.tabs.dify': 'Dify'
'roster.nodeSelector.tabs.external': 'External'
'roster.references.label': 'Workflows using {{name}}'
'roster.references.trigger': 'Workflows using {{name}}'
'roster.saveToRoster': 'Save to Agents'