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 450ea1823c0..2ce927dde5c 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
@@ -182,6 +182,20 @@ vi.mock('../components/agent-orchestrate-drawer-panel', () => ({
)
},
+ WorkflowInlineAgentConfigureWorkspace: (props: {
+ agentId?: string
+ appId?: string
+ inlineComposerState?: unknown
+ isInline: boolean
+ nodeId: string
+ open: boolean
+ }) => {
+ mockOrchestrateDrawerPanelProps.push(props)
+
+ return (
+
+ )
+ },
}))
vi.mock('../components/save-inline-agent-to-roster-dialog', () => ({
@@ -462,7 +476,7 @@ describe('agent/panel', () => {
expect(screen.queryByText(/^workflow\.errorMsg\.fieldRequired/)).not.toBeInTheDocument()
expect(container.querySelector('[aria-busy="true"]')).toBeInTheDocument()
- expect(screen.getByRole('button', { name: 'workflow.nodes.agent.roster.change' })).toBeDisabled()
+ expect(screen.getByRole('button', { name: 'workflow.nodes.agent.roster.change', hidden: true })).toBeDisabled()
expect(screen.getByRole('dialog', { name: 'workflow.nodes.agent.roster.inlineSetup.name' })).toBeInTheDocument()
expect(screen.getByRole('region', { name: 'inline-orchestrate-panel' })).toBeInTheDocument()
expect(screen.queryByText('workflow.nodes.agent.roster.editInConsole')).not.toBeInTheDocument()
@@ -478,6 +492,43 @@ describe('agent/panel', () => {
expect(container.querySelector('[inert]')).toBeInTheDocument()
})
+ it('opens a pending inline agent modal and creates the inline agent before rendering details', () => {
+ const { container, rerender } = render(
+ ,
+ )
+
+ fireEvent.click(screen.getByRole('button', { name: /^workflow\.nodes\.agent\.roster\.openPanel/ }))
+
+ expect(mockStoreState.setOpenInlineAgentPanelNodeId).toHaveBeenCalledWith('agent-node')
+ expect(mockCreateInlineAgentBinding).toHaveBeenCalledWith('agent-node', expect.objectContaining({
+ onSuccess: expect.any(Function),
+ }))
+ mockStoreState.openInlineAgentPanelNodeId = 'agent-node'
+ rerender(
+ ,
+ )
+
+ expect(screen.getByRole('dialog', { name: 'workflow.nodes.agent.roster.inlineSetup.name' })).toBeInTheDocument()
+ expect(screen.getByRole('region', { name: 'inline-orchestrate-panel' })).toBeInTheDocument()
+ expect(container.querySelector('[aria-busy="true"]')).toBeInTheDocument()
+ })
+
it('renders inline agent detail from workflow composer state and opens the inline panel', () => {
mockStoreState.openInlineAgentPanelNodeId = 'agent-node'
const { container } = render(
@@ -497,18 +548,15 @@ describe('agent/panel', () => {
expect(mockUseAgentRosterDetail).toHaveBeenCalledWith(undefined)
expect(mockUseWorkflowInlineAgentDetail).toHaveBeenCalledWith('agent-node', 'inline-agent-1')
- expect(screen.queryByText('Workflow Agent 1')).not.toBeInTheDocument()
- const trigger = screen.getByRole('button', { name: /^workflow\.nodes\.agent\.roster\.openPanel/ })
+ expect(screen.getByRole('dialog', { name: 'Workflow Agent 1' })).toBeInTheDocument()
+ const trigger = screen.getByRole('button', { name: /^workflow\.nodes\.agent\.roster\.openPanel/, hidden: true })
expect(within(trigger).getByText('workflow.nodes.agent.roster.inlineSetup.name')).toBeInTheDocument()
expect(within(trigger).getByText('workflow.nodes.agent.roster.inlineSetup.type')).toBeInTheDocument()
- const panel = screen.getByRole('dialog', { name: 'workflow.nodes.agent.roster.inlineSetup.name' })
- const panelConfigureIcon = panel.querySelector('.i-custom-vender-agent-v2-configure')
+ const panel = screen.getByRole('dialog', { name: 'Workflow Agent 1' })
expect(container.querySelector('.i-custom-vender-agent-v2-configure')).toHaveClass('h-3.5', 'w-3')
expect(container.querySelector('.i-custom-vender-agent-v2-configure')?.parentElement).toHaveClass('size-8', 'rounded-full', 'bg-background-default-burn')
- expect(panelConfigureIcon).toHaveClass('h-3.5', 'w-3')
- expect(panelConfigureIcon?.parentElement).toHaveClass('size-9', 'rounded-full', 'bg-background-default-burn')
expect(screen.queryByText('workflow.nodes.agent.roster.inlineSetup.title')).not.toBeInTheDocument()
- expect(screen.getByText('workflow.nodes.agent.roster.inlineSetup.description')).toBeInTheDocument()
+ expect(within(panel).getByText('workflow.nodes.agent.roster.inlineSetup.description')).toBeInTheDocument()
expect(screen.getByRole('region', { name: 'inline-orchestrate-panel' })).toBeInTheDocument()
expect(mockOrchestrateDrawerPanelProps.at(-1)).toMatchObject({
agentId: 'inline-agent-1',
@@ -554,14 +602,11 @@ describe('agent/panel', () => {
/>,
)
- const panel = screen.getByRole('dialog', { name: 'workflow.nodes.agent.roster.inlineSetup.name' })
+ const panel = screen.getByRole('dialog', { name: 'Workflow Agent 1' })
expect(panel).toBeInTheDocument()
- expect(panel.querySelector('header')).not.toHaveClass('h-[108px]')
- expect(panel.querySelector('.i-custom-vender-agent-v2-configure')?.parentElement).toHaveClass('size-9', 'rounded-full', 'bg-background-default-burn')
- expect(within(panel).queryByText('Workflow Agent 1')).not.toBeInTheDocument()
- expect(within(panel).getByText('workflow.nodes.agent.roster.inlineSetup.type')).toBeInTheDocument()
+ expect(within(panel).getByText('Workflow Agent 1')).toBeInTheDocument()
expect(within(panel).queryByText('workflow.nodes.agent.roster.inlineSetup.title')).not.toBeInTheDocument()
- expect(within(panel).queryByText('workflow.nodes.agent.roster.inlineSetup.description')).not.toBeInTheDocument()
+ expect(within(panel).getByText('workflow.nodes.agent.roster.inlineSetup.description')).toBeInTheDocument()
expect(within(panel).queryByRole('link', { name: 'workflow.nodes.agent.roster.editInConsole' })).not.toBeInTheDocument()
expect(within(panel).queryByRole('button', { name: 'workflow.nodes.agent.roster.makeCopy' })).not.toBeInTheDocument()
expect(screen.getByRole('region', { name: 'inline-orchestrate-panel' })).toBeInTheDocument()
@@ -583,10 +628,10 @@ describe('agent/panel', () => {
/>,
)
- const panel = screen.getByRole('dialog', { name: 'workflow.nodes.agent.roster.inlineSetup.name' })
+ 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' }))
- fireEvent.click(screen.getByRole('button', { name: 'Save inline agent to roster' }))
+ fireEvent.click(screen.getByRole('button', { name: 'Save inline agent to roster', hidden: true }))
expect(mockStoreState.setOpenInlineAgentPanelNodeId).toHaveBeenCalledWith(undefined)
expect(mockHandleNodeDataUpdateWithSyncDraft).toHaveBeenCalledWith(
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 0dff1fe9583..ad70ca413b1 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
@@ -2,8 +2,11 @@
import type { AgentConfigSnapshotSummaryResponse, AgentSoulConfig } from '@dify/contracts/api/console/agent/types.gen'
import type { WorkflowAgentComposerResponse } from '@dify/contracts/api/console/apps/types.gen'
+import { Button } from '@langgenius/dify-ui/button'
import { skipToken, useQuery } from '@tanstack/react-query'
import { useAtom } from 'jotai'
+import { useState } from 'react'
+import { useTranslation } from 'react-i18next'
import Loading from '@/app/components/base/loading'
import { ModelTypeEnum } from '@/app/components/header/account-setting/model-provider-page/declarations'
import { useDefaultModel, useTextGenerationCurrentProviderAndModelAndModelList } from '@/app/components/header/account-setting/model-provider-page/hooks'
@@ -11,6 +14,11 @@ import { AgentComposerProvider } from '@/features/agent-v2/agent-composer/provid
import { useHydrateAgentSoulConfigDraft } from '@/features/agent-v2/agent-composer/store'
import { agentComposerModelAtom } from '@/features/agent-v2/agent-composer/store-modules/model'
import { AgentOrchestratePanel } from '@/features/agent-v2/agent-detail/configure/components/orchestrate'
+import { AgentBuildPanelBackground } from '@/features/agent-v2/agent-detail/configure/components/preview/build-background'
+import { AgentBuildChat } from '@/features/agent-v2/agent-detail/configure/components/preview/build-chat'
+import { AgentPreviewHeader } from '@/features/agent-v2/agent-detail/configure/components/preview/header'
+import { AgentConfigurePreviewSurface, AgentConfigureWorkspace } from '@/features/agent-v2/agent-detail/configure/components/workspace'
+import { useAgentPreviewSoulConfig } from '@/features/agent-v2/agent-detail/configure/hooks'
import { consoleQuery } from '@/service/client'
import { useWorkflowInlineAgentConfigureSync } from '../agent-soul-config'
@@ -20,6 +28,7 @@ type AgentOrchestrateDrawerPanelProps = {
inlineComposerState?: WorkflowAgentComposerResponse
isInline: boolean
nodeId: string
+ onClose?: () => void
open: boolean
}
@@ -31,6 +40,14 @@ export function AgentOrchestrateDrawerPanel(props: AgentOrchestrateDrawerPanelPr
)
}
+export function WorkflowInlineAgentConfigureWorkspace(props: AgentOrchestrateDrawerPanelProps) {
+ return (
+
+
+
+ )
+}
+
function AgentOrchestrateDrawerPanelContent({
agentId,
appId,
@@ -99,6 +116,183 @@ function AgentOrchestrateDrawerPanelContent({
)
}
+function WorkflowInlineAgentConfigureWorkspaceContent({
+ agentId,
+ appId,
+ inlineComposerState,
+ nodeId,
+ onClose,
+ open,
+}: AgentOrchestrateDrawerPanelProps) {
+ const [clearChatList, setClearChatList] = useState(false)
+ const [conversationId, setConversationId] = useState(null)
+ const [isSaving, setIsSaving] = useState(false)
+ const composerState = inlineComposerState
+ const agentSoulConfig = composerState?.agent_soul
+ const activeConfigSnapshot = ('active_config_snapshot' in (composerState ?? {}))
+ ? composerState?.active_config_snapshot as AgentConfigSnapshotSummaryResponse | null | undefined
+ : undefined
+ const { currentModel, setConfigureModel, textGenerationModelList } = useAgentOrchestrateModelOptions()
+ const { draftSavedAt, saveDraft } = useWorkflowInlineAgentConfigureSync({
+ nodeId,
+ baseConfig: agentSoulConfig,
+ currentModel,
+ enabled: open && !!agentSoulConfig,
+ })
+ const previewAgentSoulConfig = useAgentPreviewSoulConfig(agentSoulConfig as AgentSoulConfig | undefined)
+
+ useHydrateAgentSoulConfigDraft({
+ agentId: `${nodeId}:${agentId ?? 'pending'}`,
+ activeVersionId: activeConfigSnapshot?.id,
+ config: agentSoulConfig as AgentSoulConfig | undefined,
+ })
+
+ if (!agentId || !agentSoulConfig) {
+ return (
+
+
+
+ )
+ }
+
+ const handleSave = async () => {
+ if (isSaving)
+ return
+
+ setIsSaving(true)
+ try {
+ await saveDraft()
+ onClose?.()
+ }
+ finally {
+ setIsSaving(false)
+ }
+ }
+
+ return (
+ onClose?.()}
+ onSave={() => {
+ void handleSave()
+ }}
+ />
+ )}
+ className="min-w-90"
+ onSelectModel={setConfigureModel}
+ onPublish={() => {
+ void saveDraft()
+ }}
+ onOpenVersions={() => undefined}
+ />
+ )}
+ rightPanel={(
+ }
+ header={(
+ undefined}
+ onToggleChatFeatures={() => undefined}
+ onOpenVersions={() => undefined}
+ onRefresh={() => {
+ setConversationId(null)
+ setClearChatList(true)
+ }}
+ showChatFeaturesAction={false}
+ />
+ )}
+ chat={(
+ [0]['agentIconType']}
+ agentName={composerState?.agent?.name}
+ agentSoulConfig={previewAgentSoulConfig}
+ clearChatList={clearChatList}
+ conversationId={conversationId}
+ draftType="debug_build"
+ onClearChatListChange={setClearChatList}
+ onConversationIdChange={setConversationId}
+ onSaveDraftBeforeRun={saveDraft}
+ />
+ )}
+ />
+ )}
+ />
+ )
+}
+
+function WorkflowInlineAgentConfigureActionBar({
+ isSaving,
+ onCancel,
+ onSave,
+}: {
+ isSaving: boolean
+ onCancel: () => void
+ onSave: () => void
+}) {
+ const { t } = useTranslation('common')
+
+ return (
+
+
+
+
+
+
+
+
+ )
+}
+
function useAgentOrchestrateModelOptions() {
const [model, setModel] = useAtom(agentComposerModelAtom)
const {
diff --git a/web/app/components/workflow/nodes/agent-v2/components/agent-roster-field.tsx b/web/app/components/workflow/nodes/agent-v2/components/agent-roster-field.tsx
index f28b735d984..46ad39c9cd7 100644
--- a/web/app/components/workflow/nodes/agent-v2/components/agent-roster-field.tsx
+++ b/web/app/components/workflow/nodes/agent-v2/components/agent-roster-field.tsx
@@ -3,6 +3,7 @@ import type { AgentRosterNodeData } from '@/app/components/workflow/block-select
import type { AppIconType } from '@/types/app'
import { Button } from '@langgenius/dify-ui/button'
import { cn } from '@langgenius/dify-ui/cn'
+import { Dialog, DialogCloseButton, DialogContent, DialogDescription, DialogTitle } from '@langgenius/dify-ui/dialog'
import {
Drawer,
DrawerCloseButton,
@@ -243,6 +244,60 @@ function AgentRosterDrawer({
)
}
+function AgentRosterInlineConfigureDialog({
+ agent,
+ children,
+ onSaveInlineToRoster,
+ open,
+ onClose,
+}: {
+ agent: AgentRosterDisplayData
+ children?: ReactNode
+ onSaveInlineToRoster?: () => void
+ open: boolean
+ onClose: () => void
+}) {
+ const { t } = useTranslation()
+
+ return (
+
+ )
+}
+
export function AgentRosterField({
agent,
agentId,
@@ -382,21 +437,33 @@ export function AgentRosterField({
- setPanelOpen(false)}
- >
- {panelBody}
-
+ {isInlineSetup
+ ? (
+ setPanelOpen(false)}
+ >
+ {panelBody}
+
+ )
+ : (
+ setPanelOpen(false)}
+ >
+ {panelBody}
+
+ )}
>
)
: (
diff --git a/web/app/components/workflow/nodes/agent-v2/panel.tsx b/web/app/components/workflow/nodes/agent-v2/panel.tsx
index 6948ec55cd4..fcb9e14c428 100644
--- a/web/app/components/workflow/nodes/agent-v2/panel.tsx
+++ b/web/app/components/workflow/nodes/agent-v2/panel.tsx
@@ -15,7 +15,7 @@ import { useStore } from '@/app/components/workflow/store'
import { consoleQuery } from '@/service/client'
import useNodeCrud from '../_base/hooks/use-node-crud'
import { AgentAdvancedSettings } from './components/agent-advanced-settings'
-import { AgentOrchestrateDrawerPanel } from './components/agent-orchestrate-drawer-panel'
+import { AgentOrchestrateDrawerPanel, WorkflowInlineAgentConfigureWorkspace } from './components/agent-orchestrate-drawer-panel'
import { AgentOutputVariables } from './components/agent-output-variables'
import { AgentRosterField } from './components/agent-roster-field'
import { AgentTaskField } from './components/agent-task-field'
@@ -211,6 +211,29 @@ export function AgentV2Panel({
)
}, [handleNodeDataUpdateWithSyncDraft, id, setOpenInlineAgentPanelNodeId])
+ const handleInlineAgentBindingCreated = useCallback((binding: {
+ agent_id: string
+ binding_type: 'inline_agent'
+ current_snapshot_id: string
+ }) => {
+ const newInputs = produce(inputsRef.current, (draft) => {
+ delete (draft as AgentV2NodeType & { agent_roster?: unknown }).agent_roster
+ draft.agent_binding = binding
+ draft._openInlineAgentPanel = true
+ })
+ inputsRef.current = newInputs
+ handleNodeDataUpdateWithSyncDraft(
+ {
+ id,
+ data: newInputs,
+ },
+ {
+ sync: true,
+ notRefreshWhenSyncError: true,
+ },
+ )
+ }, [handleNodeDataUpdateWithSyncDraft, id])
+
const handleStartFromScratch = useCallback(() => {
setIsRosterAgentPanelOpen(false)
setIsInlineAgentPanelOpenedFromTrigger(false)
@@ -236,38 +259,26 @@ export function AgentV2Panel({
)
createInlineAgentBinding(id, {
- onSuccess: (binding) => {
- const newInputs = produce(inputsRef.current, (draft) => {
- delete (draft as AgentV2NodeType & { agent_roster?: unknown }).agent_roster
- draft.agent_binding = binding
- draft._openInlineAgentPanel = true
- })
- inputsRef.current = newInputs
- handleNodeDataUpdateWithSyncDraft(
- {
- id,
- data: newInputs,
- },
- {
- sync: true,
- notRefreshWhenSyncError: true,
- },
- )
- },
+ onSuccess: handleInlineAgentBindingCreated,
})
- }, [createInlineAgentBinding, handleNodeDataUpdateWithSyncDraft, id, setOpenInlineAgentPanelNodeId])
+ }, [createInlineAgentBinding, handleInlineAgentBindingCreated, handleNodeDataUpdateWithSyncDraft, id, setOpenInlineAgentPanelNodeId])
const handleAgentPanelOpenChange = useCallback((open: boolean) => {
- if (isInlineAgentReady) {
+ if (isInlineAgentReady || isInlineAgentPending) {
if (open)
setIsInlineAgentPanelOpenedFromTrigger(true)
setOpenInlineAgentPanelNodeId(open ? id : undefined)
+ if (open && isInlineAgentPending && !isCreatingInlineAgent) {
+ createInlineAgentBinding(id, {
+ onSuccess: handleInlineAgentBindingCreated,
+ })
+ }
return
}
setIsRosterAgentPanelOpen(open)
- }, [id, isInlineAgentReady, setOpenInlineAgentPanelNodeId])
+ }, [createInlineAgentBinding, handleInlineAgentBindingCreated, id, isCreatingInlineAgent, isInlineAgentPending, isInlineAgentReady, setOpenInlineAgentPanelNodeId])
const handleDeclaredOutputsChange = useCallback((outputs: ReturnType, agentTask?: string) => {
const previousOutputs = getAgentV2DeclaredOutputs(inputsRef.current)
@@ -322,14 +333,28 @@ export function AgentV2Panel({
isPending={isAgentBindingPending}
panelBody={isAgentPanelOpen && displayedAgent
? (
-
+ isInlineAgentReady || isInlineAgentPending
+ ? (
+ handleAgentPanelOpenChange(false)}
+ open={isAgentPanelOpen}
+ />
+ )
+ : (
+
+ )
)
: undefined}
panelMode={isInlineAgentPending || (isInlineAgentReady && !isInlineAgentPanelOpenedFromTrigger) ? 'setup' : 'detail'}
diff --git a/web/features/agent-v2/agent-detail/configure/__tests__/page.spec.tsx b/web/features/agent-v2/agent-detail/configure/__tests__/page.spec.tsx
index 58a23e2be32..ebe461f0676 100644
--- a/web/features/agent-v2/agent-detail/configure/__tests__/page.spec.tsx
+++ b/web/features/agent-v2/agent-detail/configure/__tests__/page.spec.tsx
@@ -1,6 +1,6 @@
import type { ReactNode } from 'react'
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
-import { render, screen, waitFor } from '@testing-library/react'
+import { act, fireEvent, render, screen, waitFor } from '@testing-library/react'
import userEvent from '@testing-library/user-event'
import { AgentConfigurePage } from '../page'
@@ -196,13 +196,26 @@ vi.mock('../components/orchestrate/build-draft-bar', () => ({
vi.mock('../components/preview/build-chat', () => ({
AgentBuildChat: (props: {
conversationId?: string | null
+ onConversationComplete?: () => void
onConversationIdChange?: (conversationId: string) => void
+ onSaveDraftBeforeRun?: () => Promise
}) => (
{`build:${props.conversationId ?? 'none'}`}
+
+
),
}))
@@ -310,6 +323,10 @@ describe('AgentConfigurePage', () => {
}
})
+ afterEach(() => {
+ vi.useRealTimers()
+ })
+
describe('Loading state', () => {
it('should show loading instead of the configure panels while composer data is pending', () => {
const queryClient = new QueryClient()
@@ -487,6 +504,120 @@ describe('AgentConfigurePage', () => {
expect(screen.getByRole('region', { name: 'build-draft-bar' })).toBeInTheDocument()
})
+ it('should show the build draft bar after a new build conversation refresh completes', async () => {
+ vi.useFakeTimers()
+ const queryClient = new QueryClient()
+ const refetchBuildDraft = vi.fn().mockResolvedValue({})
+ mocks.queryState.composer = {
+ data: {
+ agent_soul: {
+ prompt: {
+ system_prompt: 'draft prompt',
+ },
+ },
+ },
+ isFetching: false,
+ isError: false,
+ isPending: false,
+ isSuccess: true,
+ refetch: vi.fn(),
+ }
+ mocks.queryState.buildDraft = {
+ data: {
+ agent_soul: {
+ prompt: {
+ system_prompt: 'build prompt',
+ },
+ },
+ draft: {},
+ variant: 'agent_app',
+ },
+ dataUpdatedAt: 1,
+ error: null,
+ isFetching: false,
+ isError: false,
+ isPending: false,
+ isSuccess: true,
+ refetch: refetchBuildDraft,
+ }
+
+ render(
+
+
+ ,
+ )
+
+ expect(screen.getByRole('region', { name: 'build-draft-bar' })).toBeInTheDocument()
+
+ fireEvent.click(screen.getByRole('button', { name: 'send build message' }))
+
+ expect(screen.queryByRole('region', { name: 'build-draft-bar' })).not.toBeInTheDocument()
+
+ fireEvent.click(screen.getByRole('button', { name: 'complete build conversation' }))
+
+ expect(refetchBuildDraft).not.toHaveBeenCalled()
+
+ await act(async () => {
+ await vi.advanceTimersByTimeAsync(1000)
+ })
+
+ expect(refetchBuildDraft).toHaveBeenCalled()
+ expect(screen.getByRole('region', { name: 'build-draft-bar' })).toBeInTheDocument()
+ })
+
+ it('should discard the build draft when restarting build mode with a build draft', async () => {
+ const user = userEvent.setup()
+ const queryClient = new QueryClient()
+ mocks.queryState.composer = {
+ data: {},
+ isFetching: false,
+ isError: false,
+ isPending: false,
+ isSuccess: true,
+ refetch: vi.fn(),
+ }
+ mocks.queryState.buildDraft = {
+ data: {
+ agent_soul: {},
+ draft: {},
+ variant: 'agent_app',
+ },
+ dataUpdatedAt: 1,
+ error: null,
+ isFetching: false,
+ isError: false,
+ isPending: false,
+ isSuccess: true,
+ refetch: vi.fn(),
+ }
+
+ render(
+
+
+ ,
+ )
+
+ await user.click(screen.getByRole('button', { name: 'restart preview' }))
+
+ await waitFor(() => expect(mocks.discardBuildDraft).toHaveBeenCalledWith(
+ {
+ params: {
+ agent_id: 'agent-1',
+ },
+ },
+ expect.any(Object),
+ ))
+ expect(mocks.refreshDebugConversation).toHaveBeenCalledWith({
+ params: {
+ agent_id: 'agent-1',
+ },
+ body: {
+ debug_conversation_id: 'debug-conversation-old',
+ },
+ }, expect.any(Object))
+ expect(screen.getByRole('region', { name: 'build-chat' })).toHaveTextContent('build:none')
+ })
+
it('should switch soul source to view version when selecting a version from build draft mode', async () => {
const user = userEvent.setup()
const queryClient = new QueryClient()
diff --git a/web/features/agent-v2/agent-detail/configure/components/preview/header.tsx b/web/features/agent-v2/agent-detail/configure/components/preview/header.tsx
index 6bcff963443..0e92e873ea3 100644
--- a/web/features/agent-v2/agent-detail/configure/components/preview/header.tsx
+++ b/web/features/agent-v2/agent-detail/configure/components/preview/header.tsx
@@ -35,6 +35,7 @@ export function AgentPreviewHeader({
onOpenVersions,
onRefresh,
refreshDisabled,
+ showChatFeaturesAction = true,
}: {
mode: AgentConfigureRightPanelMode
previewEnabled: boolean
@@ -44,6 +45,7 @@ export function AgentPreviewHeader({
onOpenVersions: () => void
onRefresh: () => void
refreshDisabled?: boolean
+ showChatFeaturesAction?: boolean
}) {
const { t } = useTranslation('agentV2')
const docLink = useDocLink()
@@ -129,20 +131,24 @@ export function AgentPreviewHeader({
)}
-
-
+ {showChatFeaturesAction && (
+ <>
+
+
+ >
+ )}
)
diff --git a/web/features/agent-v2/agent-detail/configure/components/workspace.tsx b/web/features/agent-v2/agent-detail/configure/components/workspace.tsx
new file mode 100644
index 00000000000..9f116d77c7d
--- /dev/null
+++ b/web/features/agent-v2/agent-detail/configure/components/workspace.tsx
@@ -0,0 +1,59 @@
+'use client'
+
+import type { ReactNode } from 'react'
+import { cn } from '@langgenius/dify-ui/cn'
+import { useTranslation } from 'react-i18next'
+
+type AgentConfigureWorkspaceProps = {
+ 'aria-busy'?: boolean
+ 'className'?: string
+ 'leftPanel': ReactNode
+ 'rightPanel': ReactNode
+ 'sidePanels'?: ReactNode
+}
+
+export function AgentConfigureWorkspace({
+ 'aria-busy': ariaBusy,
+ className,
+ leftPanel,
+ rightPanel,
+ sidePanels,
+}: AgentConfigureWorkspaceProps) {
+ const { t } = useTranslation('agentV2')
+
+ return (
+
+ {leftPanel}
+
+ {rightPanel}
+ {sidePanels}
+
+
+ )
+}
+
+type AgentConfigurePreviewSurfaceProps = {
+ background?: ReactNode
+ chat: ReactNode
+ header: ReactNode
+}
+
+export function AgentConfigurePreviewSurface({
+ background,
+ chat,
+ header,
+}: AgentConfigurePreviewSurfaceProps) {
+ return (
+
+ {background}
+ {header}
+
+ {chat}
+
+
+ )
+}
diff --git a/web/features/agent-v2/agent-detail/configure/page.tsx b/web/features/agent-v2/agent-detail/configure/page.tsx
index faf783bd73b..70e7df37e07 100644
--- a/web/features/agent-v2/agent-detail/configure/page.tsx
+++ b/web/features/agent-v2/agent-detail/configure/page.tsx
@@ -16,6 +16,7 @@ import { AgentChatFeaturesPanel } from './components/preview/chat-features-panel
import { AgentPreviewHeader } from './components/preview/header'
import { AgentPreviewChat } from './components/preview/preview-chat'
import { AgentPreviewVersionsPanel } from './components/preview/versions-panel'
+import { AgentConfigurePreviewSurface, AgentConfigureWorkspace } from './components/workspace'
import { useAgentConfigureData, useAgentConfigureModelOptions, useAgentPreviewSoulConfig } from './hooks'
import { useAgentConfigureBuildDraftActions, useAgentConfigureBuildDraftData } from './use-agent-configure-build-draft'
import { useAgentConfigureSync } from './use-agent-configure-sync'
@@ -90,6 +91,7 @@ function AgentConfigurePageLoadedContent({
const [showPreviewVersions, setShowPreviewVersions] = useState(false)
const [clearPreviewChat, setClearPreviewChat] = useState(false)
const [rightPanelMode, setRightPanelMode] = useState('build')
+ const [hideBuildDraftBarUntilRefresh, setHideBuildDraftBarUntilRefresh] = useState(false)
const {
agentQuery,
composerQuery,
@@ -113,6 +115,7 @@ function AgentConfigurePageLoadedContent({
isViewingVersion,
normalAgentSoulConfig: agentSoulConfig,
})
+ const showBuildDraftBar = buildDraft.isActive && !hideBuildDraftBarUntilRefresh
const refreshDebugConversationMutation = useMutation(consoleQuery.agent.byAgentId.debugConversation.refresh.post.mutationOptions({
onSuccess: ({ debug_conversation_id }) => {
queryClient.setQueryData(
@@ -162,16 +165,6 @@ function AgentConfigurePageLoadedContent({
[mode]: conversationId,
}))
}
- const restartCurrentChat = () => {
- if (rightPanelChatMode === 'build')
- refreshDebugConversation(conversationIds.build ?? '')
-
- setConversationIds(current => ({
- ...current,
- [rightPanelChatMode]: null,
- }))
- setClearPreviewChat(true)
- }
const resetBuildChatSession = useCallback(async () => {
await refreshDebugConversationAsync(conversationIds.build ?? '')
setConversationIds(current => ({
@@ -215,6 +208,21 @@ function AgentConfigurePageLoadedContent({
buildDraft.setSoulSourceOverride(versionId ? 'view-version' : null)
onSelectVersion(versionId)
}, [buildDraft, onSelectVersion])
+ const restartCurrentChat = () => {
+ if (rightPanelChatMode === 'build' && buildDraft.isActive) {
+ void buildDraftActions.discardBuildDraft()
+ return
+ }
+
+ if (rightPanelChatMode === 'build')
+ refreshDebugConversation(conversationIds.build ?? '')
+
+ setConversationIds(current => ({
+ ...current,
+ [rightPanelChatMode]: null,
+ }))
+ setClearPreviewChat(true)
+ }
if (buildDraft.isPending) {
return (
@@ -229,62 +237,60 @@ function AgentConfigurePageLoadedContent({
}
return (
-
- {
- void buildDraftActions.applyBuildDraft()
- }}
- onDiscard={() => {
- void buildDraftActions.discardBuildDraft()
- }}
- />
- )
- : undefined}
- onSelectModel={setConfigureModel}
- onPublish={publishDraft}
- onOpenVersions={() => setShowPreviewVersions(true)}
- onExitVersions={() => selectVersion(null)}
- />
-
- {/* Preview area */}
-
-
-
-
setShowChatFeatures(open => !open)}
- onOpenVersions={() => setShowPreviewVersions(true)}
- onRefresh={restartCurrentChat}
- refreshDisabled={isRefreshingDebugConversation}
- />
-
-
+ leftPanel={(
+
{
+ void buildDraftActions.applyBuildDraft()
+ }}
+ onDiscard={() => {
+ void buildDraftActions.discardBuildDraft()
+ }}
+ />
+ )
+ : undefined}
+ onSelectModel={setConfigureModel}
+ onPublish={publishDraft}
+ onOpenVersions={() => setShowPreviewVersions(true)}
+ onExitVersions={() => selectVersion(null)}
+ />
+ )}
+ rightPanel={(
+ }
+ header={(
+ setShowChatFeatures(open => !open)}
+ onOpenVersions={() => setShowPreviewVersions(true)}
+ onRefresh={restartCurrentChat}
+ refreshDisabled={isRefreshingDebugConversation || buildDraftActions.isDiscardingBuildDraft}
+ />
+ )}
+ chat={(
{
+ if (mode === 'build')
+ buildDraftActions.refreshBuildDraftAfterBuildChat(() => setHideBuildDraftBarUntilRefresh(false))
+ }}
onConversationIdChange={updateConversationId}
- onSaveDraftBeforeRun={rightPanelChatMode === 'build' ? buildDraftActions.prepareBuildDraftBeforeRun : saveDraft}
+ onSaveDraftBeforeRun={rightPanelChatMode === 'build'
+ ? async () => {
+ setHideBuildDraftBarUntilRefresh(true)
+ await buildDraftActions.prepareBuildDraftBeforeRun()
+ }
+ : saveDraft}
/>
-
-
-
- {showPreviewVersions && (
-
setShowPreviewVersions(false)}
- />
- )}
- setShowChatFeatures(false)}
+ )}
/>
-
-
+ )}
+ sidePanels={(
+ <>
+ {showPreviewVersions && (
+ setShowPreviewVersions(false)}
+ />
+ )}
+ setShowChatFeatures(false)}
+ />
+ >
+ )}
+ />
)
}
diff --git a/web/features/agent-v2/agent-detail/configure/use-agent-configure-build-draft.ts b/web/features/agent-v2/agent-detail/configure/use-agent-configure-build-draft.ts
index 87e67ddfe22..865d4e4c3e2 100644
--- a/web/features/agent-v2/agent-detail/configure/use-agent-configure-build-draft.ts
+++ b/web/features/agent-v2/agent-detail/configure/use-agent-configure-build-draft.ts
@@ -157,13 +157,14 @@ export function useAgentConfigureBuildDraftActions({
setSoulSourceOverride('build-draft')
}, [agentId, buildDraftQueryOptions.queryKey, checkoutBuildDraft, isActive, queryClient, saveDraft, setSoulSourceOverride])
- const refreshBuildDraftAfterBuildChat = useCallback(() => {
+ const refreshBuildDraftAfterBuildChat = useCallback((onRefreshed?: () => void) => {
if (buildDraftRefreshTimerRef.current)
clearTimeout(buildDraftRefreshTimerRef.current)
- buildDraftRefreshTimerRef.current = setTimeout(() => {
+ buildDraftRefreshTimerRef.current = setTimeout(async () => {
buildDraftRefreshTimerRef.current = null
- void refetchBuildDraft()
+ await refetchBuildDraft()
+ onRefreshed?.()
}, 1000)
}, [refetchBuildDraft])