fix(web): simplify agent configure mode state (#39492)

This commit is contained in:
yyh 2026-07-24 13:30:45 +08:00 committed by GitHub
parent abbad7b313
commit 6e2c21f568
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
2 changed files with 27 additions and 72 deletions

View File

@ -79,9 +79,7 @@ const modelHooksState = vi.hoisted(() => ({
const editionState = vi.hoisted(() => ({
isSelfHosted: false,
isSystemFeaturesPending: false,
licenseStatus: 'none',
systemFeaturesPendingPromise: new Promise<never>(() => {}),
}))
function createDeferredPromise<T>() {
@ -169,17 +167,13 @@ vi.mock('@tanstack/react-query', async (importOriginal) => {
isSuccess: false,
}
}),
useSuspenseQuery: vi.fn(() => {
if (editionState.isSystemFeaturesPending) throw editionState.systemFeaturesPendingPromise
return {
data: {
license: {
status: editionState.licenseStatus,
},
useSuspenseQuery: vi.fn(() => ({
data: {
license: {
status: editionState.licenseStatus,
},
}
}),
},
})),
}
})
@ -528,7 +522,6 @@ describe('AgentConfigurePage', () => {
vi.clearAllMocks()
mocks.completeBuildConversation = undefined
editionState.isSelfHosted = false
editionState.isSystemFeaturesPending = false
editionState.licenseStatus = 'none'
modelHooksState.defaultTextGenerationModel = {
provider: {
@ -597,22 +590,6 @@ describe('AgentConfigurePage', () => {
})
describe('Loading state', () => {
it('should keep the configure loading UI while system features are loading', () => {
editionState.isSystemFeaturesPending = true
render(
<QueryClientProvider client={new QueryClient()}>
<AgentConfigurePage agentId="agent-1" />
</QueryClientProvider>,
)
const configureSection = screen.getByRole('region', {
name: 'agentV2.agentDetail.sections.configure',
})
expect(configureSection).toHaveAttribute('aria-busy', 'true')
expect(screen.getByRole('status', { name: 'appApi.loading' })).toBeInTheDocument()
})
it('should show the page loading indicator instead of skeleton panels while composer data is pending', () => {
const queryClient = new QueryClient()
@ -764,10 +741,6 @@ describe('AgentConfigurePage', () => {
{ searchParams: '?source=shared-link' },
)
await waitFor(() => {
expect(onUrlUpdate.mock.calls.at(-1)?.[0].searchParams.get('mode')).toBe('build')
})
await user.click(screen.getByRole('button', { name: 'preview mode' }))
let urlUpdate = onUrlUpdate.mock.calls.at(-1)?.[0]
@ -778,7 +751,7 @@ describe('AgentConfigurePage', () => {
await user.click(screen.getByRole('button', { name: 'build mode' }))
urlUpdate = onUrlUpdate.mock.calls.at(-1)?.[0]
expect(urlUpdate?.searchParams.get('mode')).toBe('build')
expect(urlUpdate?.searchParams.has('mode')).toBe(false)
expect(urlUpdate?.searchParams.get('source')).toBe('shared-link')
})
@ -831,7 +804,7 @@ describe('AgentConfigurePage', () => {
}),
).toBeInTheDocument()
expect(mocks.discardBuildDraft).not.toHaveBeenCalled()
expect(onUrlUpdate.mock.calls.at(-1)?.[0].searchParams.get('mode')).toBe('build')
expect(onUrlUpdate).not.toHaveBeenCalled()
await user.click(screen.getByRole('button', { name: 'common.operation.confirm' }))
@ -899,7 +872,7 @@ describe('AgentConfigurePage', () => {
expect(
screen.getByText('agentV2.agentDetail.configure.clearSessionConfirm.description'),
).toBeInTheDocument()
expect(onUrlUpdate.mock.calls.at(-1)?.[0].searchParams.get('mode')).toBe('build')
expect(onUrlUpdate).not.toHaveBeenCalled()
expect(mocks.discardBuildDraft).not.toHaveBeenCalled()
await user.click(screen.getByRole('button', { name: 'common.operation.cancel' }))
@ -915,7 +888,7 @@ describe('AgentConfigurePage', () => {
expect(mocks.stopBuildChat).toHaveBeenCalledTimes(1)
expect(mocks.discardBuildDraft).toHaveBeenCalledTimes(1)
expectFirstMockCallBefore(mocks.stopBuildChat, mocks.discardBuildDraft)
expect(onUrlUpdate.mock.calls.at(-1)?.[0].searchParams.get('mode')).toBe('build')
expect(onUrlUpdate).not.toHaveBeenCalled()
const completeBuildConversation = mocks.completeBuildConversation
if (!completeBuildConversation) throw new Error('Expected a Build completion callback.')
@ -1012,7 +985,7 @@ describe('AgentConfigurePage', () => {
expect(toastMock.error).toHaveBeenCalledWith('common.api.actionFailed')
})
expect(confirmButton).not.toHaveAttribute('aria-disabled', 'true')
expect(onUrlUpdate.mock.calls.at(-1)?.[0].searchParams.get('mode')).toBe('build')
expect(onUrlUpdate).not.toHaveBeenCalled()
expect(screen.getByRole('alertdialog')).toBeInTheDocument()
const completeBuildConversation = mocks.completeBuildConversation
@ -1544,7 +1517,7 @@ describe('AgentConfigurePage', () => {
expect(screen.getByRole('region', { name: 'preview-chat' })).toHaveTextContent('preview:none')
})
it('should keep preview disabled in community edition', async () => {
it('should keep preview disabled in community edition', () => {
editionState.isSelfHosted = true
mocks.queryState.composer = {
data: {},
@ -1555,7 +1528,7 @@ describe('AgentConfigurePage', () => {
refetch: vi.fn(),
}
const { onUrlUpdate } = render(
render(
<QueryClientProvider client={new QueryClient()}>
<AgentConfigurePage agentId="agent-1" />
</QueryClientProvider>,
@ -1566,9 +1539,6 @@ describe('AgentConfigurePage', () => {
expect(screen.getByRole('region', { name: 'build-chat' })).toHaveTextContent(
'build:debug-conversation-old',
)
await waitFor(() => {
expect(onUrlUpdate.mock.calls.at(-1)?.[0].searchParams.get('mode')).toBe('build')
})
})
it('should enable preview for a self-hosted enterprise license', () => {

View File

@ -5,7 +5,7 @@ import { useSuspenseQuery } from '@tanstack/react-query'
import { useAtomValue, useSetAtom } from 'jotai'
import { ScopeProvider } from 'jotai-scope'
import { parseAsStringLiteral, useQueryState } from 'nuqs'
import { Suspense, useCallback, useEffect } from 'react'
import { useCallback } from 'react'
import { useTranslation } from 'react-i18next'
import { IS_CE_EDITION } from '@/config'
import { systemFeaturesQueryOptions } from '@/features/system-features/client'
@ -22,32 +22,25 @@ import {
rebaseAgentConfigureComposerAtom,
} from './state'
const agentConfigureModeQueryParser = parseAsStringLiteral(
AGENT_CONFIGURE_RIGHT_PANEL_MODES,
).withOptions({ history: 'replace' })
const agentConfigureModeQueryParser = parseAsStringLiteral(AGENT_CONFIGURE_RIGHT_PANEL_MODES)
.withDefault('build')
.withOptions({ history: 'replace' })
type AgentConfigurePageProps = {
agentId: string
}
export function AgentConfigurePage({ agentId }: AgentConfigurePageProps) {
const { t } = useTranslation('agentV2')
const loadingLabel = t(($) => $['agentDetail.sections.configure'])
return (
<Suspense fallback={<AgentConfigurePageLoading label={loadingLabel} />}>
<ScopeProvider key={agentId} atoms={agentConfigureScopedAtoms} name="AgentConfigure">
<AgentConfigurePageContent agentId={agentId} loadingLabel={loadingLabel} />
</ScopeProvider>
</Suspense>
<ScopeProvider key={agentId} atoms={agentConfigureScopedAtoms} name="AgentConfigure">
<AgentConfigurePageContent agentId={agentId} />
</ScopeProvider>
)
}
function AgentConfigurePageContent({
agentId,
loadingLabel,
}: AgentConfigurePageProps & { loadingLabel: string }) {
const [modeInUrl, setModeInUrl] = useQueryState('mode', agentConfigureModeQueryParser)
function AgentConfigurePageContent({ agentId }: AgentConfigurePageProps) {
const { t } = useTranslation('agentV2')
const [requestedMode, setRequestedMode] = useQueryState('mode', agentConfigureModeQueryParser)
const selectedVersionId = useAtomValue(agentConfigureSelectedVersionIdAtom)
const composerRebaseRevision = useAtomValue(agentConfigureComposerRebaseRevisionAtom)
const rebaseComposer = useSetAtom(rebaseAgentConfigureComposerAtom)
@ -58,26 +51,18 @@ function AgentConfigurePageContent({
!IS_CE_EDITION ||
systemFeatures.license.status === LicenseStatus.ACTIVE ||
systemFeatures.license.status === LicenseStatus.EXPIRING
const requestedMode = modeInUrl ?? 'build'
const rightPanelMode = previewEnabled ? requestedMode : 'build'
const rightPanelMode = requestedMode === 'preview' && previewEnabled ? 'preview' : 'build'
const changeRightPanelMode = useCallback(
(nextMode: AgentConfigureRightPanelMode) => {
if (nextMode === 'preview' && !previewEnabled) return
return setModeInUrl(nextMode)
return setRequestedMode(nextMode)
},
[previewEnabled, setModeInUrl],
[previewEnabled, setRequestedMode],
)
useEffect(() => {
if (modeInUrl === rightPanelMode) return
// oxlint-disable-next-line eslint-react/set-state-in-effect -- The URL is external state and must mirror the effective mode after parsing and license gating.
void setModeInUrl(rightPanelMode, { history: 'replace' })
}, [modeInUrl, rightPanelMode, setModeInUrl])
if (configureData.isPending) {
return <AgentConfigurePageLoading label={loadingLabel} />
return <AgentConfigurePageLoading label={t(($) => $['agentDetail.sections.configure'])} />
}
return (