fix(web): initialize empty agent models from workspace defaults (#42736)

This commit is contained in:
Joel authored and GitHub committed 2026-09-23 02:51:25 +00:00
1 parent 654a49d1bc
commit 960bea8bae
9 files changed
+515 -14

No files matched your search

@@ -9,9 +9,11 @@ import { agentComposerDraftAtom, agentComposerSavedDraftAtom } from './store'
export function AgentComposerProvider({
children,
initialDraft,
initialSavedDraft = initialDraft,
}: {
children: ReactNode
initialDraft?: AgentSoulConfigFormState
initialSavedDraft?: AgentSoulConfigFormState
}) {
const draft = initialDraft ?? defaultAgentSoulConfigFormState
@@ -19,7 +21,7 @@ export function AgentComposerProvider({
<ScopeProvider
atoms={[
[agentComposerDraftAtom, draft],
[agentComposerSavedDraftAtom, draft],
[agentComposerSavedDraftAtom, initialSavedDraft ?? draft],
]}
name="AgentComposer"
>
@@ -150,6 +150,9 @@ vi.mock('@tanstack/react-query', async (importOriginal) => {
useQuery: vi.fn((options: { queryKey?: readonly string[] }) => {
const queryKey = options.queryKey?.[0]
if (queryKey === 'default-model') {
return { data: { data: modelHooksState.defaultTextGenerationModel }, isPending: false }
}
if (queryKey === 'agent') return mocks.queryState.agent
if (queryKey === 'composer') return mocks.queryState.composer
if (queryKey === 'version') return mocks.queryState.version
@@ -212,6 +215,13 @@ vi.mock('@/service/console', () => ({
queryOptions: () => ({ queryKey: ['system-features'] }),
},
},
workspaces: {
current: {
defaultModel: {
get: { queryOptions: () => ({ queryKey: ['default-model'] }) },
},
},
},
agent: {
get: {
key: () => ['agents'],
@@ -317,6 +327,7 @@ vi.mock('../components/orchestrate', async () => {
return {
AgentOrchestratePanel: (props: {
bottomAction?: ReactNode
currentModel?: { model: string }
isBuildDraftActive?: boolean
onExitVersions?: () => void
onOpenVersions?: () => void
@@ -333,6 +344,7 @@ vi.mock('../components/orchestrate', async () => {
<span>{`readonly:${props.readOnly ? 'yes' : 'no'}`}</span>
<span>{`publish:${props.showPublishBar ? 'yes' : 'no'}`}</span>
<span>{`prompt:${prompt}`}</span>
<span>{`model:${props.currentModel?.model ?? 'none'}`}</span>
<button type="button" onClick={() => setPrompt('edited draft prompt')}>
edit prompt
</button>
@@ -2340,7 +2352,62 @@ describe('AgentConfigurePage', () => {
expect(screen.getByRole('button', { name: 'discard build draft' })).toBeDisabled()
})
it('should require an explicit model even when the workspace has a default', async () => {
it('should save the workspace default model before starting a build', async () => {
const queryClient = createQueryClient()
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: undefined as unknown,
dataUpdatedAt: 0,
error: new Response(null, { status: 404 }),
isFetching: false,
isError: true,
isPending: false,
isSuccess: false,
refetch: vi.fn(),
}
render(
<QueryClientProvider client={queryClient}>
<AgentConfigurePage agentId="agent-1" />
</QueryClientProvider>,
)
fireEvent.click(screen.getByRole('button', { name: 'send build message' }))
await waitFor(() => {
expect(mocks.checkoutBuildDraft).toHaveBeenCalled()
})
expect(mocks.saveComposerDraft).toHaveBeenCalledWith(
expect.objectContaining({
body: expect.objectContaining({
agent_soul: expect.objectContaining({
model: expect.objectContaining({
model_provider: 'langgenius/openai/openai',
model: 'gpt-4o-mini',
}),
}),
}),
}),
expect.anything(),
)
expectFirstMockCallBefore(mocks.saveComposerDraft, mocks.checkoutBuildDraft)
})
it('should require a model when neither the draft nor the workspace has one', async () => {
modelHooksState.defaultTextGenerationModel = undefined
const queryClient = createQueryClient()
mocks.queryState.composer = {
data: {
@@ -3380,6 +3447,9 @@ describe('AgentConfigurePage', () => {
</QueryClientProvider>,
)
expect(screen.getByRole('region', { name: 'orchestrate-panel' })).toHaveTextContent(
'model:none',
)
await user.click(screen.getByRole('button', { name: 'discard build draft' }))
await waitFor(() =>
@@ -3401,6 +3471,11 @@ describe('AgentConfigurePage', () => {
expect.any(Object),
)
expect(screen.getByRole('region', { name: 'build-chat' })).toHaveTextContent('build:none')
await waitFor(() => {
expect(screen.getByRole('region', { name: 'orchestrate-panel' })).toHaveTextContent(
'model:gpt-4o-mini',
)
})
})
it('should keep exiting build draft when debug conversation refresh fails after discarding build draft', async () => {
@@ -13,6 +13,7 @@ import { defaultAgentSoulConfigFormState } from '@/features/agent-v2/agent-compo
import {
agentComposerDraftAtom,
agentComposerSavedDraftAtom,
isAgentComposerDirtyAtom,
} from '@/features/agent-v2/agent-composer/store'
import { agentComposerFilesAtom } from '@/features/agent-v2/agent-composer/store-modules/files'
import { agentComposerPromptAtom } from '@/features/agent-v2/agent-composer/store-modules/prompt'
@@ -240,6 +241,7 @@ function renderUseAgentConfigureSync({
agentName = 'Agent',
baseConfig,
currentModel,
savedDraft,
enabled = true,
publishEnabled,
suspend = false,
@@ -247,6 +249,7 @@ function renderUseAgentConfigureSync({
agentName?: Parameters<typeof useAgentConfigureSync>[0]['agentName']
baseConfig?: Parameters<typeof useAgentConfigureSync>[0]['baseConfig']
currentModel?: AgentSoulConfigFormState['model']
savedDraft?: AgentSoulConfigFormState
enabled?: boolean
publishEnabled?: boolean
suspend?: boolean
@@ -266,7 +269,7 @@ function renderUseAgentConfigureSync({
? { ...defaultAgentSoulConfigFormState, model: currentModel }
: defaultAgentSoulConfigFormState
store.set(agentComposerDraftAtom, initialDraft)
store.set(agentComposerSavedDraftAtom, initialDraft)
store.set(agentComposerSavedDraftAtom, savedDraft ?? initialDraft)
const pendingRender = new Promise<void>(() => {})
const wrapper = ({ children }: PropsWithChildren) => (
<QueryClientProvider client={queryClient}>
@@ -499,6 +502,105 @@ describe('useAgentConfigureSync', () => {
)
})
it('autosaves an initialized default model without another edit', async () => {
const { store } = renderUseAgentConfigureSync({
currentModel: configuredModel,
savedDraft: defaultAgentSoulConfigFormState,
})
await act(async () => {
await vi.advanceTimersByTimeAsync(5000)
})
expect(composerPutMutationFn).toHaveBeenCalledExactlyOnceWith(
expect.objectContaining({
body: expect.objectContaining({
agent_soul: expect.objectContaining({
model: expect.objectContaining({
model_provider: configuredModel.provider,
model: configuredModel.model,
}),
}),
}),
}),
)
expect(store.get(isAgentComposerDirtyAtom)).toBe(false)
})
it('serializes a newly selected model after an in-flight default model save', async () => {
const savingDefault = createDeferredPromise<{ agent_soul: Record<string, unknown> }>()
composerPutMutationFn.mockReturnValueOnce(savingDefault.promise)
const { store } = renderUseAgentConfigureSync({
currentModel: configuredModel,
savedDraft: defaultAgentSoulConfigFormState,
})
await act(async () => {
await vi.advanceTimersByTimeAsync(5000)
})
expect(composerPutMutationFn).toHaveBeenCalledTimes(1)
const selectedModel = { ...configuredModel, model: 'gpt-4o' }
act(() => {
store.set(agentComposerDraftAtom, {
...store.get(agentComposerDraftAtom),
model: selectedModel,
})
})
await act(async () => {
await vi.advanceTimersByTimeAsync(5000)
})
expect(composerPutMutationFn).toHaveBeenCalledTimes(1)
const savingSelection = createDeferredPromise<{ agent_soul: Record<string, unknown> }>()
composerPutMutationFn.mockReturnValueOnce(savingSelection.promise)
await act(async () => {
savingDefault.resolve({ agent_soul: {} })
})
expect(store.get(agentComposerDraftAtom).model).toEqual(selectedModel)
expect(store.get(isAgentComposerDirtyAtom)).toBe(true)
expect(composerPutMutationFn).toHaveBeenNthCalledWith(
2,
expect.objectContaining({
body: expect.objectContaining({
agent_soul: expect.objectContaining({
model: expect.objectContaining({ model: selectedModel.model }),
}),
}),
}),
)
await act(async () => {
savingSelection.resolve({ agent_soul: {} })
})
expect(store.get(agentComposerDraftAtom).model).toEqual(selectedModel)
expect(store.get(isAgentComposerDirtyAtom)).toBe(false)
})
it('autosaves an existing dirty model when saving becomes enabled', async () => {
const { rerender } = renderUseAgentConfigureSync({
currentModel: configuredModel,
savedDraft: defaultAgentSoulConfigFormState,
enabled: false,
})
await act(async () => {
await vi.advanceTimersByTimeAsync(5000)
})
expect(composerPutMutationFn).not.toHaveBeenCalled()
rerender({
agentName: 'Agent',
baseConfig: undefined,
currentModel: configuredModel,
enabled: true,
suspend: false,
})
await act(async () => {
await vi.advanceTimersByTimeAsync(5000)
})
expect(composerPutMutationFn).toHaveBeenCalledTimes(1)
})
it('should cancel pending autosave when the draft returns to the saved baseline', async () => {
const { queryClient, store } = renderUseAgentConfigureSync()
queryClient.setQueryData(['agent-detail', 'agent-1'], {
@@ -0,0 +1,170 @@
import type { AgentSoulConfig } from '@dify/contracts/api/console/agent/types.gen'
import type {
DefaultModelDataResponse,
SimpleProviderEntityResponse,
} from '@dify/contracts/api/console/workspaces/types.gen'
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
import { act, render, screen } from '@testing-library/react'
import userEvent from '@testing-library/user-event'
import { useAtom, useAtomValue } from 'jotai'
import { beforeEach, describe, expect, it, vi } from 'vite-plus/test'
import {
agentComposerSavedDraftAtom,
isAgentComposerDirtyAtom,
} from '@/features/agent-v2/agent-composer/store'
import { agentComposerModelAtom } from '@/features/agent-v2/agent-composer/store-modules/model'
import { AgentConfigureComposerProvider } from '../composer-provider'
const mocks = vi.hoisted(() => ({
getDefaultModel: vi.fn<() => Promise<DefaultModelDataResponse>>(),
}))
vi.mock('@/service/console', () => ({
consoleQuery: {
workspaces: {
current: {
defaultModel: {
get: {
queryOptions: ({ enabled }: { enabled: boolean }) => ({
queryKey: ['default-model'],
queryFn: mocks.getDefaultModel,
enabled,
}),
},
},
},
},
},
}))
function ModelEditor() {
const [model, setModel] = useAtom(agentComposerModelAtom)
const savedDraft = useAtomValue(agentComposerSavedDraftAtom)
const isDirty = useAtomValue(isAgentComposerDirtyAtom)
return (
<div>
<label>
Model
<input
value={model?.model ?? ''}
onChange={(event) =>
setModel({ provider: 'langgenius/openai/openai', model: event.target.value })
}
/>
</label>
<output aria-label="Saved model">{savedDraft?.model?.model ?? 'None'}</output>
<output aria-label="Unsaved changes">{String(isDirty)}</output>
</div>
)
}
function renderComposer(initialConfig: AgentSoulConfig = {}, initializeDefaultModel = true) {
const queryClient = new QueryClient({ defaultOptions: { queries: { retry: false } } })
render(
<QueryClientProvider client={queryClient}>
<AgentConfigureComposerProvider
initialConfig={initialConfig}
initializeDefaultModel={initializeDefaultModel}
>
<ModelEditor />
</AgentConfigureComposerProvider>
</QueryClientProvider>,
)
return queryClient
}
const provider: SimpleProviderEntityResponse = {
provider: 'langgenius/openai/openai',
label: { en_US: 'OpenAI', zh_Hans: 'OpenAI' },
supported_model_types: ['llm'],
tenant_id: 'workspace-1',
}
const defaultModel: DefaultModelDataResponse = {
data: { model: 'gpt-4o-mini', model_type: 'llm', provider },
}
describe('Agent configure default model initialization', () => {
beforeEach(() => {
vi.clearAllMocks()
mocks.getDefaultModel.mockResolvedValue(defaultModel)
})
it('waits for the default before exposing an editable draft and preserves the unsaved baseline', async () => {
let resolve!: (value: DefaultModelDataResponse) => void
mocks.getDefaultModel.mockReturnValue(
new Promise((promiseResolve) => {
resolve = promiseResolve
}),
)
renderComposer()
expect(screen.queryByRole('textbox', { name: 'Model' })).not.toBeInTheDocument()
await act(async () => resolve(defaultModel))
expect(await screen.findByRole('textbox', { name: 'Model' })).toHaveValue('gpt-4o-mini')
expect(screen.getByLabelText('Saved model')).toHaveTextContent('None')
expect(screen.getByLabelText('Unsaved changes')).toHaveTextContent('true')
})
it('preserves an explicitly configured model without fetching the default', () => {
renderComposer({
model: {
model: 'configured-model',
model_provider: 'openai',
plugin_id: 'langgenius/openai',
},
})
expect(screen.getByRole('textbox', { name: 'Model' })).toHaveValue('configured-model')
expect(screen.getByLabelText('Saved model')).toHaveTextContent('configured-model')
expect(screen.getByLabelText('Unsaved changes')).toHaveTextContent('false')
expect(mocks.getDefaultModel).not.toHaveBeenCalled()
})
it('leaves model-less configurations unchanged when initialization is disabled', () => {
renderComposer({}, false)
expect(screen.getByRole('textbox', { name: 'Model' })).toHaveValue('')
expect(screen.getByLabelText('Unsaved changes')).toHaveTextContent('false')
expect(mocks.getDefaultModel).not.toHaveBeenCalled()
})
it.each(['missing', 'error'] as const)(
'allows manual selection when the default is %s',
async (result) => {
const user = userEvent.setup()
if (result === 'missing') mocks.getDefaultModel.mockResolvedValue({ data: null })
else mocks.getDefaultModel.mockRejectedValue(new Error('Default model unavailable'))
renderComposer()
const input = await screen.findByRole('textbox', { name: 'Model' })
expect(input).toHaveValue('')
await user.type(input, 'manual-model')
expect(input).toHaveValue('manual-model')
expect(screen.getByLabelText('Unsaved changes')).toHaveTextContent('true')
},
)
it('preserves the user selection when another consumer refreshes the default model', async () => {
const user = userEvent.setup()
const queryClient = renderComposer()
const input = await screen.findByRole('textbox', { name: 'Model' })
await user.clear(input)
await user.type(input, 'manual-model')
mocks.getDefaultModel.mockResolvedValue({
data: { model: 'new-default', model_type: 'llm', provider },
})
await act(async () => {
await queryClient.query({ queryKey: ['default-model'], queryFn: mocks.getDefaultModel })
})
expect(input).toHaveValue('manual-model')
expect(screen.getByLabelText('Saved model')).toHaveTextContent('None')
expect(screen.getByLabelText('Unsaved changes')).toHaveTextContent('true')
})
})
@@ -0,0 +1,65 @@
'use client'
import type { AgentSoulConfig } from '@dify/contracts/api/console/agent/types.gen'
import type { ReactNode } from 'react'
import type { AgentSoulConfigFormState } from '@/features/agent-v2/agent-composer/form-state'
import { useQuery } from '@tanstack/react-query'
import { useState } from 'react'
import { useTranslation } from 'react-i18next'
import { agentSoulConfigToFormState } from '@/features/agent-v2/agent-composer/conversions'
import { AgentComposerProvider } from '@/features/agent-v2/agent-composer/provider'
import { consoleQuery } from '@/service/console'
import { AgentConfigurePageLoading } from './page-loading'
export function AgentConfigureComposerProvider({
children,
initialConfig,
initializeDefaultModel,
}: {
children: ReactNode
initialConfig?: AgentSoulConfig
initializeDefaultModel: boolean
}) {
const { t } = useTranslation('agentV2')
const [session, setSession] = useState<{
draft: AgentSoulConfigFormState
savedDraft: AgentSoulConfigFormState
}>()
const savedDraft = agentSoulConfigToFormState(initialConfig)
const needsDefaultModel = initializeDefaultModel && !savedDraft.model
const defaultModelQuery = useQuery(
consoleQuery.workspaces.current.defaultModel.get.queryOptions({
input: { query: { model_type: 'llm' } },
enabled: !session && needsDefaultModel,
}),
)
if (!session) {
if (needsDefaultModel && defaultModelQuery.isPending) {
return <AgentConfigurePageLoading label={t(($) => $['agentDetail.sections.configure'])} />
}
const defaultModel = defaultModelQuery.data?.data
// Capture defaults once for this keyed form session. Later query responses must not reset edits.
setSession({
savedDraft,
draft:
needsDefaultModel && defaultModel
? {
...savedDraft,
model: {
provider: defaultModel.provider.provider,
model: defaultModel.model,
},
}
: savedDraft,
})
return null
}
return (
<AgentComposerProvider initialDraft={session.draft} initialSavedDraft={session.savedDraft}>
{children}
</AgentComposerProvider>
)
}
@@ -16,7 +16,6 @@ import { useTranslation } from 'react-i18next'
import { LoadingPlaceholder } from '@/app/components/base/loading-placeholder'
import { toast } from '@/app/notifications'
import { agentSoulConfigToFormState } from '@/features/agent-v2/agent-composer/conversions'
import { AgentComposerProvider } from '@/features/agent-v2/agent-composer/provider'
import { rebaseAgentComposerDraftAtom } from '@/features/agent-v2/agent-composer/store'
import { agentComposerModelAtom } from '@/features/agent-v2/agent-composer/store-modules/model'
import {
@@ -39,6 +38,7 @@ import {
} from '../use-agent-configure-build-draft'
import { useAgentConfigureSessionController } from '../use-agent-configure-session-controller'
import { useAgentConfigureSync } from '../use-agent-configure-sync'
import { AgentConfigureComposerProvider } from './composer-provider'
import { AgentConfigureClearSessionConfirmDialog } from './confirm-clear-session-dialog'
import { AgentOrchestratePanel } from './orchestrate'
import { AgentBuildDraftBar } from './orchestrate/build-draft-bar'
@@ -225,9 +225,15 @@ function AgentConfigurePageComposerSession({
]}
name="AgentConfigureConversation"
>
<AgentComposerProvider
<AgentConfigureComposerProvider
key={composerSessionKey}
initialDraft={agentSoulConfigToFormState(buildDraft.agentSoulConfig)}
initialConfig={buildDraft.agentSoulConfig}
initializeDefaultModel={
configureData.capabilities.canEdit &&
configureData.composerQuery.isSuccess &&
!isViewingVersion &&
!buildDraft.isActive
}
>
<AgentConfigurePageComposerContent
agentId={agentId}
@@ -247,7 +253,7 @@ function AgentConfigurePageComposerSession({
onRefreshPreviewConversation={refreshPreviewConversation}
onSelectVersion={onSelectVersion}
/>
</AgentComposerProvider>
</AgentConfigureComposerProvider>
</ScopeProvider>
)
}
@@ -349,7 +355,7 @@ function AgentConfigurePageComposerContent({
},
[rebaseComposerDraft],
)
const [currentModel, setConfigureModel] = useAtom(agentComposerModelAtom)
const [currentModel, setCurrentModel] = useAtom(agentComposerModelAtom)
const { isPublishing, publishDraft, saveDraft } = useAgentConfigureSync({
agentId,
agentName: agentQuery.data?.name,
@@ -511,7 +517,7 @@ function AgentConfigurePageComposerContent({
/>
) : undefined
}
onSelectModel={setConfigureModel}
onSelectModel={setCurrentModel}
onPublish={publishDraft}
onOpenVersions={() => {
workingDirectoryPanel.closeWorkingDirectory()
@@ -3,9 +3,15 @@ import type { AgentComposerModel } from '@/features/agent-v2/agent-composer/form
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
import { act, render, screen, waitFor } from '@testing-library/react'
import userEvent from '@testing-library/user-event'
import {
ConfigurationMethodEnum,
ModelStatusEnum,
ModelTypeEnum,
} from '@/app/components/header/account-setting/model-provider-page/declarations'
import { AgentModelField } from '../field'
const providerSummary = vi.hoisted(() => vi.fn())
const credentialPanelState = vi.hoisted(() => vi.fn())
const modelList = vi.hoisted(() => vi.fn<() => Promise<{ data: ProviderWithModelsResponse[] }>>())
@@ -21,7 +27,7 @@ vi.mock('@/service/use-common', () => ({
vi.mock(
'@/app/components/header/account-setting/model-provider-page/provider-added-card/use-credential-panel-state',
() => ({
useCredentialPanelState: () => ({ variant: 'api-unavailable', hasCredentials: false }),
useCredentialPanelState: credentialPanelState,
}),
)
@@ -75,6 +81,7 @@ describe('AgentModelField', () => {
beforeEach(() => {
vi.clearAllMocks()
providerSummary.mockResolvedValue({ data: [] })
credentialPanelState.mockReturnValue({ variant: 'api-unavailable', hasCredentials: false })
})
it('disables model controls until the catalog loads without selecting a model', async () => {
@@ -147,6 +154,71 @@ describe('AgentModelField', () => {
expect(onSelect).not.toHaveBeenCalled()
})
it.each(['compatible', 'incompatible'] as const)(
'waits for the catalog after provider metadata before showing a %s model status',
async (compatibility) => {
const modelId = compatibility === 'compatible' ? 'gemini-3.8-flash' : 'gemini-2.0-flash'
const modelLabel = compatibility === 'compatible' ? 'Gemini 3.8 Flash' : 'Gemini 2.0 Flash'
const provider: ProviderWithModelsResponse = {
tenant_id: 'test-workspace',
provider: 'google',
label: { en_US: 'Google', zh_Hans: 'Google' },
icon_small: { en_US: '', zh_Hans: '' },
icon_small_dark: { en_US: '', zh_Hans: '' },
status: ModelStatusEnum.active,
models: [
{
model: modelId,
label: { en_US: modelLabel, zh_Hans: modelLabel },
model_type: ModelTypeEnum.textGeneration,
features: [],
fetch_from: ConfigurationMethodEnum.predefinedModel,
status: ModelStatusEnum.active,
model_properties: { mode: 'chat' },
load_balancing_enabled: false,
},
],
}
let resolveCatalog!: (value: { data: ProviderWithModelsResponse[] }) => void
modelList.mockImplementation(
() =>
new Promise((resolve) => {
resolveCatalog = resolve
}),
)
providerSummary.mockResolvedValue({ data: [provider] })
credentialPanelState.mockReturnValue({ variant: 'api-active', hasCredentials: true })
const { queryClient, onSelect } = renderField({ provider: 'google', model: modelId })
await waitFor(() => {
expect(queryClient.getQueryState(['providers'])?.status).toBe('success')
})
expect(screen.getByRole('button', { name: modelId })).toBeDisabled()
expect(
screen.queryByText('common.modelProvider.selector.incompatible'),
).not.toBeInTheDocument()
await act(async () => {
resolveCatalog({ data: [provider] })
})
await waitFor(() => {
expect(screen.getByRole('button', { name: new RegExp(modelLabel) })).toBeEnabled()
})
if (compatibility === 'compatible') {
expect(screen.getByRole('button', { name: modelLabel })).toHaveAttribute(
'data-model-status',
'active',
)
expect(
screen.queryByText('common.modelProvider.selector.incompatible'),
).not.toBeInTheDocument()
} else {
expect(screen.getByText('common.modelProvider.selector.incompatible')).toBeInTheDocument()
}
expect(onSelect).not.toHaveBeenCalled()
},
)
it('keeps a failed initial load disabled and lets the user retry', async () => {
const user = userEvent.setup()
modelList.mockRejectedValueOnce(new Error('Unavailable')).mockResolvedValue({ data: [] })
@@ -405,7 +405,13 @@ export function useAgentConfigureBuildDraftActions({
queryKey: buildDraftQueryOptions.queryKey,
})
rebaseComposerDraft(nextAgentSoulConfig)
if (shouldRefetchComposer) onComposerRebased?.()
if (
shouldRefetchComposer ||
!nextAgentSoulConfig?.model?.model ||
!nextAgentSoulConfig.model.model_provider
) {
onComposerRebased?.()
}
},
[
buildDraftQueryOptions.queryKey,
@@ -374,7 +374,7 @@ export function useAgentConfigureSync({
)
useEffect(() => {
return store.sub(agentComposerDraftAtom, () => {
const scheduleDirtyDraftSave = () => {
const agentSoulDraft = getAgentSoulDraft()
const agentSoulDraftKey = JSON.stringify(agentSoulDraft)
const isDirty = store.get(isAgentComposerDirtyAtom)
@@ -389,8 +389,11 @@ export function useAgentConfigureSync({
}
debouncedSaveDraft()
})
}, [debouncedSaveDraft, getAgentSoulDraft, store])
}
scheduleDirtyDraftSave()
return store.sub(agentComposerDraftAtom, scheduleDirtyDraftSave)
}, [debouncedSaveDraft, enabled, getAgentSoulDraft, store])
useEffect(() => {
const saveDraftWhenPageHidden = () => {