fix: smooth the preview-to-build transition (#39495)

This commit is contained in:
Joel 2026-07-24 14:30:07 +08:00 committed by GitHub
parent aef1c67721
commit 6889974c05
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
6 changed files with 89 additions and 32 deletions

View File

@ -370,6 +370,7 @@ vi.mock('../components/preview/build-chat', async () => {
clearChatList?: boolean
conversationId?: string | null
controllerRef?: Ref<{ stop: () => void }>
disabled?: boolean
onConversationComplete?: (conversationId: string) => void
onConversationIdChange?: (conversationId: string) => void
onBeforeSpeechToText?: () => Promise<unknown>
@ -397,6 +398,7 @@ vi.mock('../components/preview/build-chat', async () => {
</button>
<button
type="button"
disabled={props.disabled}
onClick={() => {
void props
.onSaveDraftBeforeRun?.()
@ -755,6 +757,58 @@ describe('AgentConfigurePage', () => {
expect(urlUpdate?.searchParams.get('source')).toBe('shared-link')
})
it('should show an editable composer and an empty Build chat while starting a fresh Build session', async () => {
const user = userEvent.setup()
const refreshBuildConversation = createDeferredPromise<{
debug_conversation_has_messages: boolean
debug_conversation_id: string
debug_conversation_message_count: number
}>()
mocks.refreshDebugConversation.mockReturnValueOnce(refreshBuildConversation.promise)
mocks.queryState.composer = {
data: {
agent_soul: {
prompt: {
system_prompt: 'draft prompt',
},
},
},
isFetching: false,
isError: false,
isPending: false,
isSuccess: true,
refetch: vi.fn(),
}
render(
<QueryClientProvider client={new QueryClient()}>
<AgentConfigureComposerScopeHarness />
</QueryClientProvider>,
)
await user.click(screen.getByRole('button', { name: 'build mode' }))
expect(screen.getByRole('region', { name: 'orchestrate-panel' })).toHaveTextContent(
'readonly:no',
)
expect(screen.getByRole('region', { name: 'orchestrate-panel' })).toHaveTextContent(
'publish:yes',
)
expect(screen.getByRole('region', { name: 'build-chat' })).toHaveTextContent('build:none')
expect(screen.queryByRole('status', { name: 'appApi.loading' })).not.toBeInTheDocument()
expect(screen.getByRole('button', { name: 'send build message' })).toBeDisabled()
refreshBuildConversation.resolve({
debug_conversation_has_messages: false,
debug_conversation_id: 'debug-conversation-new',
debug_conversation_message_count: 0,
})
await waitFor(() => {
expect(screen.getByRole('button', { name: 'send build message' })).toBeEnabled()
})
})
it('should confirm before discarding an existing build draft and switching to preview', async () => {
const user = userEvent.setup()
mocks.queryState.composer = {

View File

@ -310,13 +310,10 @@ function AgentConfigurePageComposerContent({
const queryClient = useQueryClient()
const showBuildDraftBar = buildDraft.isActive
const resetBuildChatState = useCallback(async () => {
try {
await onRefreshDebugConversationAsync()
} finally {
setCompletedBuildConversationId(null)
setConversationId({ mode: 'build', conversationId: null })
setClearChatByMode((current) => ({ ...current, build: true }))
}
setCompletedBuildConversationId(null)
setConversationId({ mode: 'build', conversationId: null })
setClearChatByMode((current) => ({ ...current, build: true }))
await onRefreshDebugConversationAsync()
}, [onRefreshDebugConversationAsync, setClearChatByMode, setConversationId])
const rebaseComposerDraftFromSoulConfig = useCallback(
(agentSoulConfig?: AgentSoulConfig) => {
@ -458,16 +455,11 @@ function AgentConfigurePageComposerContent({
textGenerationModelList={textGenerationModelList}
draftSavedAt={draftSavedAt}
isPublishing={isPublishing}
readOnly={
isViewingVersion ||
buildDraft.isActive ||
buildDraftActionsDisabled ||
isEnteringBuildMode
}
readOnly={isViewingVersion || buildDraft.isActive || buildDraftActionsDisabled}
selectedVersionSnapshot={isViewingVersion ? activeConfigSnapshot : undefined}
isBuildDraftActive={buildDraft.isActive}
buildDraftChangedKeys={buildDraft.changedKeys}
showPublishBar={!buildDraft.isActive && !isEnteringBuildMode}
showPublishBar={!buildDraft.isActive}
workflowReferencesEnabled={agentQuery.isSuccess}
bottomAction={
showBuildDraftBar ? (
@ -519,7 +511,7 @@ function AgentConfigurePageComposerContent({
/>
}
chat={
buildDraft.isPending || isEnteringBuildMode ? (
buildDraft.isPending ? (
<Loading type="app" />
) : (
<AgentConfigureRightPanelChat
@ -532,6 +524,7 @@ function AgentConfigurePageComposerContent({
clearChatList={clearChatByMode[rightPanelChatMode]}
controllerRef={rightPanelChatControllerRef}
conversationIds={conversationIds}
disabled={isEnteringBuildMode}
mode={rightPanelChatMode}
speechToTextDraftType={
rightPanelChatMode === 'build' && buildDraft.isActive ? 'debug_build' : 'draft'

View File

@ -30,6 +30,7 @@ export type AgentChatRuntimeProps = {
clearChatList: boolean
controllerRef?: Ref<AgentPreviewChatController>
conversationId?: string | null
disabled?: boolean
draftType?: 'debug_build'
speechToTextDraftType?: 'draft' | 'debug_build'
inputPlaceholder: string
@ -57,6 +58,7 @@ export function AgentChatRuntime({
clearChatList,
controllerRef,
conversationId,
disabled,
draftType,
speechToTextDraftType,
inputPlaceholder,
@ -136,6 +138,7 @@ export function AgentChatRuntime({
clearChatList={clearChatList}
controllerRef={controllerRef}
conversationId={conversationId}
disabled={disabled}
draftType={draftType}
speechToTextDraftType={speechToTextDraftType}
initialChatTree={initialChatTree}

View File

@ -34,6 +34,7 @@ export function AgentPreviewChatSession({
clearChatList,
controllerRef,
conversationId,
disabled,
draftType,
speechToTextDraftType,
initialChatTree,
@ -61,6 +62,7 @@ export function AgentPreviewChatSession({
clearChatList: boolean
controllerRef?: Ref<AgentPreviewChatController>
conversationId?: string | null
disabled?: boolean
draftType?: 'debug_build'
speechToTextDraftType?: 'draft' | 'debug_build'
initialChatTree: ChatItemInTree[]
@ -115,8 +117,11 @@ export function AgentPreviewChatSession({
files?: FileEntity[],
isRegenerate: boolean = false,
parentAnswer: ChatItem | null = null,
) => conversationRef.current?.send(message, files, isRegenerate, parentAnswer),
[],
) => {
if (disabled) return
return conversationRef.current?.send(message, files, isRegenerate, parentAnswer)
},
[disabled],
)
useImperativeHandle(
controllerRef,
@ -141,7 +146,7 @@ export function AgentPreviewChatSession({
<ChatInputArea
botName={agentName || 'Agent'}
customPlaceholder={inputPlaceholder}
disabled={isEmptyChat && isResponding}
disabled={disabled || (isEmptyChat && isResponding)}
// Build chat opts out so it does not steal focus from the configure editor.
// oxlint-disable-next-line jsx-a11y/no-autofocus
autoFocus={isEmptyChat ? inputAutoFocus : undefined}

View File

@ -365,10 +365,10 @@ export function useAgentConfigureBuildDraftActions({
const startFreshBuildSession = useCallback(async () => {
cancelBuildDraftRefresh()
setSoulSourceOverride('draft')
try {
await resetBuildChatSession()
forceCheckoutBeforeNextBuildRunRef.current = true
setSoulSourceOverride('draft')
return true
} catch {
toast.error(tCommon(($) => $['api.actionFailed']))

View File

@ -214,21 +214,23 @@ export function useAgentConfigureSessionController({
if (pendingBuildModeTransitionRef.current) return
setIsEnteringBuildMode(true)
modeRef.current = 'build'
rotateBuildCallbackGeneration(true)
const buildSessionStart = startFreshBuildSession()
const modeChange = (async () => onModeChange(nextMode))()
const transition = (async () => {
try {
const started = await startFreshBuildSession()
if (!started || modeRef.current !== 'preview') return
const [buildSessionResult, modeChangeResult] = await Promise.allSettled([
buildSessionStart,
modeChange,
])
const buildSessionStarted =
buildSessionResult.status === 'fulfilled' && buildSessionResult.value
const modeChanged = modeChangeResult.status === 'fulfilled'
if ((buildSessionStarted && modeChanged) || modeRef.current !== 'build') return
modeRef.current = 'build'
rotateBuildCallbackGeneration(true)
try {
await onModeChange(nextMode)
} catch (error) {
modeRef.current = 'preview'
rotateBuildCallbackGeneration(false)
throw error
}
} catch {}
modeRef.current = 'preview'
rotateBuildCallbackGeneration(false)
await Promise.resolve(onModeChange('preview')).catch(() => undefined)
})()
pendingBuildModeTransitionRef.current = transition
void transition.finally(() => {