diff --git a/web/app/components/workflow/nodes/agent-v2/__tests__/hooks.spec.tsx b/web/app/components/workflow/nodes/agent-v2/__tests__/hooks.spec.tsx
index ece36b121b2..089182565ba 100644
--- a/web/app/components/workflow/nodes/agent-v2/__tests__/hooks.spec.tsx
+++ b/web/app/components/workflow/nodes/agent-v2/__tests__/hooks.spec.tsx
@@ -348,4 +348,52 @@ describe('useWorkflowInlineAgentConfigureSync', () => {
}),
}))
})
+
+ it('still saves manually when inline agent autosave is disabled', async () => {
+ const queryClient = new QueryClient({
+ defaultOptions: {
+ queries: {
+ retry: false,
+ },
+ mutations: {
+ retry: false,
+ },
+ },
+ })
+ const { result } = renderWorkflowHook(() => useWorkflowInlineAgentConfigureSync({
+ nodeId: 'node-1',
+ baseConfig: {
+ schema_version: 1,
+ },
+ autoSaveEnabled: false,
+ enabled: true,
+ }), {
+ queryClient,
+ hooksStoreProps: {
+ configsMap: {
+ flowId: 'app-1',
+ flowType: FlowType.appFlow,
+ fileSettings: {} as never,
+ },
+ },
+ })
+
+ await act(async () => {
+ await result.current.saveDraft()
+ })
+
+ expect(mockComposerMutationFn).toHaveBeenCalledWith({
+ params: {
+ app_id: 'app-1',
+ node_id: 'node-1',
+ },
+ body: expect.objectContaining({
+ variant: 'workflow',
+ save_strategy: 'node_job_only',
+ agent_soul: expect.objectContaining({
+ schema_version: 1,
+ }),
+ }),
+ }, expect.any(Object))
+ })
})
diff --git a/web/app/components/workflow/nodes/agent-v2/__tests__/panel.spec.tsx b/web/app/components/workflow/nodes/agent-v2/__tests__/panel.spec.tsx
index 2ce927dde5c..95a31c3cf5c 100644
--- a/web/app/components/workflow/nodes/agent-v2/__tests__/panel.spec.tsx
+++ b/web/app/components/workflow/nodes/agent-v2/__tests__/panel.spec.tsx
@@ -20,6 +20,7 @@ const {
mockSetInputs,
mockStoreState,
mockUseAgentRosterDetail,
+ mockWorkflowInlineAgentDetailRefetch,
mockUseWorkflowInlineAgentDetail,
mockUseNodeCrud,
} = vi.hoisted(() => ({
@@ -48,6 +49,7 @@ const {
setOpenInlineAgentPanelNodeId: vi.fn(),
},
mockUseAgentRosterDetail: vi.fn(),
+ mockWorkflowInlineAgentDetailRefetch: vi.fn(),
mockUseWorkflowInlineAgentDetail: vi.fn(),
mockUseNodeCrud: vi.fn(),
}))
@@ -188,12 +190,39 @@ vi.mock('../components/agent-orchestrate-drawer-panel', () => ({
inlineComposerState?: unknown
isInline: boolean
nodeId: string
+ onClose?: () => void
+ onSaved?: (binding: {
+ agent_id?: string | null
+ binding_type: 'inline_agent' | 'roster_agent'
+ current_snapshot_id?: string | null
+ }) => void
+ onSaveInlineToRoster?: () => void
open: boolean
}) => {
mockOrchestrateDrawerPanelProps.push(props)
return (
-
+
+
+
+
)
},
}))
@@ -339,6 +368,8 @@ describe('agent/panel', () => {
},
}
: undefined,
+ isFetching: false,
+ refetch: mockWorkflowInlineAgentDetailRefetch,
}))
})
@@ -586,6 +617,7 @@ describe('agent/panel', () => {
fireEvent.click(screen.getByRole('button', { name: /^workflow\.nodes\.agent\.roster\.openPanel/ }))
expect(mockStoreState.setOpenInlineAgentPanelNodeId).toHaveBeenCalledWith('agent-node')
+ expect(mockWorkflowInlineAgentDetailRefetch).toHaveBeenCalled()
mockStoreState.openInlineAgentPanelNodeId = 'agent-node'
rerender(
@@ -612,7 +644,7 @@ describe('agent/panel', () => {
expect(screen.getByRole('region', { name: 'inline-orchestrate-panel' })).toBeInTheDocument()
})
- it('opens save-to-roster action from the inline drawer menu and rebinds to the saved roster agent', () => {
+ it('opens save-to-roster action from the inline workspace menu and rebinds to the saved roster agent', () => {
mockStoreState.openInlineAgentPanelNodeId = 'agent-node'
render(
{
)
const panel = screen.getByRole('dialog', { name: 'Workflow Agent 1' })
- fireEvent.click(within(panel).getByRole('button', { name: 'workflow.nodes.agent.roster.more' }))
- fireEvent.click(screen.getByRole('menuitem', { name: 'agentV2.roster.saveToRoster' }))
+ expect(within(panel).queryByRole('button', { name: 'workflow.nodes.agent.roster.more' })).not.toBeInTheDocument()
+ fireEvent.click(within(panel).getByRole('button', { name: 'Inline workspace more' }))
fireEvent.click(screen.getByRole('button', { name: 'Save inline agent to roster', hidden: true }))
expect(mockStoreState.setOpenInlineAgentPanelNodeId).toHaveBeenCalledWith(undefined)
@@ -651,6 +683,43 @@ describe('agent/panel', () => {
)
})
+ it('updates the inline binding snapshot from the modal save response before closing', () => {
+ mockStoreState.openInlineAgentPanelNodeId = 'agent-node'
+ render(
+ ,
+ )
+
+ fireEvent.click(screen.getByRole('button', { name: 'Save inline modal' }))
+
+ expect(mockHandleNodeDataUpdateWithSyncDraft).toHaveBeenCalledWith(
+ {
+ id: 'agent-node',
+ data: expect.objectContaining({
+ agent_binding: {
+ binding_type: 'inline_agent',
+ agent_id: 'inline-agent-1',
+ current_snapshot_id: 'latest-snapshot-2',
+ },
+ }),
+ },
+ expect.objectContaining({
+ sync: true,
+ notRefreshWhenSyncError: true,
+ }),
+ )
+ expect(mockStoreState.setOpenInlineAgentPanelNodeId).toHaveBeenCalledWith(undefined)
+ })
+
it('does not show start from scratch for an existing inline agent binding', () => {
render(
{
it('opens the inline panel while workflow composer state is still loading', () => {
mockStoreState.openInlineAgentPanelNodeId = 'agent-node'
- mockUseWorkflowInlineAgentDetail.mockReturnValue({ data: undefined })
+ mockUseWorkflowInlineAgentDetail.mockReturnValue({
+ data: undefined,
+ isFetching: true,
+ refetch: mockWorkflowInlineAgentDetailRefetch,
+ })
const { container } = render(
{
+ const saveComposer = useSerialAsyncCallback(async (configSnapshot: AgentSoulConfig): Promise => {
if (!configsMap?.flowId || configsMap.flowType !== FlowType.appFlow)
return
@@ -121,6 +123,7 @@ export function useWorkflowInlineAgentConfigureSync({
setOriginalDraft(agentSoulConfigToFormState(composerState.agent_soul))
setDraftSavedAt(Date.now())
lastAutosavedDraftKeyRef.current = savedDraftKey
+ return composerState
})
const latestDraftSaveRef = useRef<() => void>(() => undefined)
@@ -137,7 +140,7 @@ export function useWorkflowInlineAgentConfigureSync({
return
debouncedSaveDraft.cancel?.()
- await saveComposer(getAgentSoulDraft())
+ return saveComposer(getAgentSoulDraft())
}, [debouncedSaveDraft, getAgentSoulDraft, saveComposer])
useEffect(() => {
@@ -147,6 +150,7 @@ export function useWorkflowInlineAgentConfigureSync({
if (
!enabledRef.current
+ || !autoSaveEnabled
|| !store.get(isAgentComposerDirtyAtom)
|| lastAutosavedDraftKeyRef.current === agentSoulDraftKey
) {
@@ -155,13 +159,14 @@ export function useWorkflowInlineAgentConfigureSync({
debouncedSaveDraft()
})
- }, [debouncedSaveDraft, getAgentSoulDraft, store])
+ }, [autoSaveEnabled, debouncedSaveDraft, getAgentSoulDraft, store])
useEffect(() => {
return () => {
- debouncedSaveDraft.flush?.()
+ if (autoSaveEnabled)
+ debouncedSaveDraft.flush?.()
}
- }, [debouncedSaveDraft])
+ }, [autoSaveEnabled, debouncedSaveDraft])
return {
draftSavedAt,
diff --git a/web/app/components/workflow/nodes/agent-v2/components/agent-orchestrate-drawer-panel.tsx b/web/app/components/workflow/nodes/agent-v2/components/agent-orchestrate-drawer-panel.tsx
index ad70ca413b1..c2350b7009d 100644
--- a/web/app/components/workflow/nodes/agent-v2/components/agent-orchestrate-drawer-panel.tsx
+++ b/web/app/components/workflow/nodes/agent-v2/components/agent-orchestrate-drawer-panel.tsx
@@ -1,8 +1,14 @@
'use client'
import type { AgentConfigSnapshotSummaryResponse, AgentSoulConfig } from '@dify/contracts/api/console/agent/types.gen'
-import type { WorkflowAgentComposerResponse } from '@dify/contracts/api/console/apps/types.gen'
+import type { AgentComposerBindingResponse, WorkflowAgentComposerResponse } from '@dify/contracts/api/console/apps/types.gen'
import { Button } from '@langgenius/dify-ui/button'
+import {
+ DropdownMenu,
+ DropdownMenuContent,
+ DropdownMenuItem,
+ DropdownMenuTrigger,
+} from '@langgenius/dify-ui/dropdown-menu'
import { skipToken, useQuery } from '@tanstack/react-query'
import { useAtom } from 'jotai'
import { useState } from 'react'
@@ -29,6 +35,8 @@ type AgentOrchestrateDrawerPanelProps = {
isInline: boolean
nodeId: string
onClose?: () => void
+ onSaved?: (binding: AgentComposerBindingResponse) => void
+ onSaveInlineToRoster?: () => void
open: boolean
}
@@ -122,8 +130,11 @@ function WorkflowInlineAgentConfigureWorkspaceContent({
inlineComposerState,
nodeId,
onClose,
+ onSaved,
+ onSaveInlineToRoster,
open,
}: AgentOrchestrateDrawerPanelProps) {
+ const { t } = useTranslation()
const [clearChatList, setClearChatList] = useState(false)
const [conversationId, setConversationId] = useState(null)
const [isSaving, setIsSaving] = useState(false)
@@ -137,6 +148,7 @@ function WorkflowInlineAgentConfigureWorkspaceContent({
nodeId,
baseConfig: agentSoulConfig,
currentModel,
+ autoSaveEnabled: false,
enabled: open && !!agentSoulConfig,
})
const previewAgentSoulConfig = useAgentPreviewSoulConfig(agentSoulConfig as AgentSoulConfig | undefined)
@@ -161,7 +173,17 @@ function WorkflowInlineAgentConfigureWorkspaceContent({
setIsSaving(true)
try {
- await saveDraft()
+ const composerState = await saveDraft()
+ const binding = composerState?.binding
+ if (
+ binding?.binding_type !== 'inline_agent'
+ || !binding.agent_id
+ || !binding.current_snapshot_id
+ ) {
+ return
+ }
+
+ onSaved?.(binding)
onClose?.()
}
finally {
@@ -188,6 +210,7 @@ function WorkflowInlineAgentConfigureWorkspaceContent({
onClose?.()}
+ onSaveInlineToRoster={onSaveInlineToRoster}
onSave={() => {
void handleSave()
}}
@@ -217,6 +240,16 @@ function WorkflowInlineAgentConfigureWorkspaceContent({
setClearChatList(true)
}}
showChatFeaturesAction={false}
+ trailingAction={(
+
+ )}
/>
)}
chat={(
@@ -232,7 +265,6 @@ function WorkflowInlineAgentConfigureWorkspaceContent({
draftType="debug_build"
onClearChatListChange={setClearChatList}
onConversationIdChange={setConversationId}
- onSaveDraftBeforeRun={saveDraft}
/>
)}
/>
@@ -244,10 +276,12 @@ function WorkflowInlineAgentConfigureWorkspaceContent({
function WorkflowInlineAgentConfigureActionBar({
isSaving,
onCancel,
+ onSaveInlineToRoster,
onSave,
}: {
isSaving: boolean
onCancel: () => void
+ onSaveInlineToRoster?: () => void
onSave: () => void
}) {
const { t } = useTranslation('common')
@@ -268,16 +302,28 @@ function WorkflowInlineAgentConfigureActionBar({
-
+
+
+
+
+ )}
+ />
+
+
+
+ {t('roster.saveToRoster', { ns: 'agentV2' })}
+
+
+
>
)}
+ {trailingAction && (
+ <>
+
+ {trailingAction}
+ >
+ )}
)