@@ -3,8 +3,16 @@ import { useQueryClient } from '@tanstack/react-query';
33import { __ } from '@wordpress/i18n' ;
44import { IconButton } from '@wordpress/ui' ;
55import { clsx } from 'clsx' ;
6- import { useCallback , useLayoutEffect , useMemo , useRef , useState } from 'react' ;
7- import { Composer } from '@/components/session-view/composer' ;
6+ import {
7+ useCallback ,
8+ useLayoutEffect ,
9+ useMemo ,
10+ useRef ,
11+ useState ,
12+ type ReactNode ,
13+ type Ref ,
14+ } from 'react' ;
15+ import { Composer , ComposerSkeleton } from '@/components/session-view/composer' ;
816import { pickLiveSite } from '@/components/session-view/composer/environment-pill' ;
917import { Conversation } from '@/components/session-view/conversation' ;
1018import { EmptyBackground } from '@/components/session-view/empty-background' ;
@@ -87,6 +95,29 @@ function SessionHeader( {
8795 ) ;
8896}
8997
98+ interface SessionFrameProps {
99+ header ?: ReactNode ;
100+ composer ?: ReactNode ;
101+ preview ?: ReactNode ;
102+ scrollRef ?: Ref < HTMLDivElement > ;
103+ children ?: ReactNode ;
104+ }
105+
106+ function SessionFrame ( { header, composer, preview, scrollRef, children } : SessionFrameProps ) {
107+ return (
108+ < div className = { styles . root } >
109+ < div className = { styles . chatColumn } >
110+ { header }
111+ < div ref = { scrollRef } className = { styles . scroll } >
112+ { children }
113+ </ div >
114+ < div className = { styles . composerOuter } > { composer } </ div >
115+ </ div >
116+ { preview }
117+ </ div >
118+ ) ;
119+ }
120+
90121export function SessionView ( { sessionId } : { sessionId : string } ) {
91122 const { data, isLoading, error } = useSession ( sessionId ) ;
92123 const connector = useConnector ( ) ;
@@ -165,7 +196,22 @@ export function SessionView( { sessionId }: { sessionId: string } ) {
165196 } , [ sessionId , data , isRunning , queuedPrompts . length ] ) ;
166197
167198 if ( isLoading ) {
168- return < div className = { styles . state } > { __ ( 'Loading session…' ) } </ div > ;
199+ // Use the same SessionFrame with an empty header and a structural
200+ // ComposerSkeleton so the scroll area has the exact same dimensions
201+ // as the loaded view — otherwise the EmptyBackground canvas jumps
202+ // mid-transition.
203+ return (
204+ < SessionFrame
205+ header = { < div className = { styles . header } /> }
206+ composer = {
207+ < div className = { styles . column } >
208+ < ComposerSkeleton />
209+ </ div >
210+ }
211+ >
212+ < EmptyBackground />
213+ </ SessionFrame >
214+ ) ;
169215 }
170216
171217 if ( error || ! data ) {
@@ -178,48 +224,48 @@ export function SessionView( { sessionId }: { sessionId: string } ) {
178224 }
179225
180226 return (
181- < div className = { styles . root } >
182- < div className = { styles . chatColumn } >
227+ < SessionFrame
228+ scrollRef = { scrollRef }
229+ header = {
183230 < SessionHeader
184231 summary = { data . summary }
185232 previewOpen = { showPreview }
186233 onTogglePreview = { ( ) => setPreviewOpen ( ( open ) => ! open ) }
187234 canTogglePreview = { canTogglePreview }
188235 />
189- < div ref = { scrollRef } className = { styles . scroll } >
190- { isEmpty ? < EmptyBackground /> : null }
191- < div className = { clsx ( styles . column , styles . conversationSpacing ) } >
192- < Conversation
193- data = { data }
194- isRunning = { isRunning }
195- startedAt = { startedAt }
196- pendingQuestions = { pendingQuestionTexts }
197- pendingAnswers = { pendingAnswers }
198- onAnswerQuestion = { answerQuestion }
199- />
200- </ div >
201- </ div >
202- < div className = { styles . composerOuter } >
203- < div className = { styles . column } >
204- < QueuedPrompts prompts = { queuedPrompts } onRemove = { removeQueuedPrompt } />
205- < Composer
206- busy = { composerBusy }
207- isInterrupting = { isInterrupting }
208- error = { runError }
209- model = { currentModel }
210- onModelChange = { onModelChange }
211- onSend = { sendMessage }
212- onInterrupt = { interrupt }
213- sessionId = { sessionId }
214- effectiveEnvironment = { effectiveEnvironment }
215- liveSite = { liveSite }
216- />
217- </ div >
236+ }
237+ composer = {
238+ < div className = { styles . column } >
239+ < QueuedPrompts prompts = { queuedPrompts } onRemove = { removeQueuedPrompt } />
240+ < Composer
241+ busy = { composerBusy }
242+ isInterrupting = { isInterrupting }
243+ error = { runError }
244+ model = { currentModel }
245+ onModelChange = { onModelChange }
246+ onSend = { sendMessage }
247+ onInterrupt = { interrupt }
248+ sessionId = { sessionId }
249+ effectiveEnvironment = { effectiveEnvironment }
250+ liveSite = { liveSite }
251+ />
218252 </ div >
253+ }
254+ preview = {
255+ showPreview && ownerSite ? < SitePreview site = { ownerSite } sessionId = { sessionId } /> : null
256+ }
257+ >
258+ { isEmpty ? < EmptyBackground /> : null }
259+ < div className = { clsx ( styles . column , styles . conversationSpacing ) } >
260+ < Conversation
261+ data = { data }
262+ isRunning = { isRunning }
263+ startedAt = { startedAt }
264+ pendingQuestions = { pendingQuestionTexts }
265+ pendingAnswers = { pendingAnswers }
266+ onAnswerQuestion = { answerQuestion }
267+ />
219268 </ div >
220- { showPreview && ownerSite ? (
221- < SitePreview site = { ownerSite } sessionId = { sessionId } />
222- ) : null }
223- </ div >
269+ </ SessionFrame >
224270 ) ;
225271}
0 commit comments