@@ -193,6 +193,220 @@ async function findMainContentIframe(
193193 return null ;
194194}
195195
196+ /**
197+ * Takes a full-page screenshot of a scrollable container by temporarily
198+ * expanding it to show all scrollable content.
199+ */
200+ async function takeScrollableContainerFullPageScreenshot (
201+ containerHandle : ElementHandle < Element > ,
202+ page : Page ,
203+ options : { type : 'png' | 'jpeg' | 'webp' ; quality ?: number } ,
204+ ) : Promise < Uint8Array > {
205+ // Get the full scroll dimensions of the container
206+ const { scrollWidth, scrollHeight} = await containerHandle . evaluate ( el => {
207+ return {
208+ scrollWidth : el . scrollWidth ,
209+ scrollHeight : el . scrollHeight ,
210+ } ;
211+ } ) ;
212+
213+ // Get the original container styles to restore later
214+ const originalStyle = await containerHandle . evaluate ( el => {
215+ const htmlEl = el as HTMLElement ;
216+ return {
217+ width : htmlEl . style . width ,
218+ height : htmlEl . style . height ,
219+ maxWidth : htmlEl . style . maxWidth ,
220+ maxHeight : htmlEl . style . maxHeight ,
221+ overflow : htmlEl . style . overflow ,
222+ overflowX : htmlEl . style . overflowX ,
223+ overflowY : htmlEl . style . overflowY ,
224+ position : htmlEl . style . position ,
225+ } ;
226+ } ) ;
227+
228+ // Store original ancestor styles that might clip the container
229+ const originalAncestorStyles = await containerHandle . evaluate ( el => {
230+ const styles : Array < {
231+ element : HTMLElement ;
232+ overflow : string ;
233+ overflowX : string ;
234+ overflowY : string ;
235+ maxHeight : string ;
236+ height : string ;
237+ } > = [ ] ;
238+ let parent = el . parentElement ;
239+ while ( parent && parent !== document . body && parent !== document . documentElement ) {
240+ const computed = getComputedStyle ( parent ) ;
241+ if ( computed . overflow !== 'visible' || computed . overflowY !== 'visible' ) {
242+ styles . push ( {
243+ element : parent ,
244+ overflow : parent . style . overflow ,
245+ overflowX : parent . style . overflowX ,
246+ overflowY : parent . style . overflowY ,
247+ maxHeight : parent . style . maxHeight ,
248+ height : parent . style . height ,
249+ } ) ;
250+ }
251+ parent = parent . parentElement ;
252+ }
253+ return styles . length ;
254+ } ) ;
255+
256+ try {
257+ // Scroll to top-left to ensure we capture from the beginning
258+ await containerHandle . evaluate ( el => {
259+ el . scrollTo ( 0 , 0 ) ;
260+ } ) ;
261+
262+ // Temporarily expand the container and disable overflow clipping on ancestors
263+ await containerHandle . evaluate (
264+ ( el , data ) => {
265+ const htmlEl = el as HTMLElement ;
266+
267+ // Expand the container
268+ htmlEl . style . width = `${ data . scrollWidth } px` ;
269+ htmlEl . style . height = `${ data . scrollHeight } px` ;
270+ htmlEl . style . maxWidth = 'none' ;
271+ htmlEl . style . maxHeight = 'none' ;
272+ htmlEl . style . overflow = 'visible' ;
273+ htmlEl . style . overflowX = 'visible' ;
274+ htmlEl . style . overflowY = 'visible' ;
275+ htmlEl . style . position = 'absolute' ;
276+
277+ // Disable clipping on ancestor elements
278+ let parent = el . parentElement ;
279+ while ( parent && parent !== document . body && parent !== document . documentElement ) {
280+ const computed = getComputedStyle ( parent ) ;
281+ if ( computed . overflow !== 'visible' || computed . overflowY !== 'visible' ) {
282+ parent . style . overflow = 'visible' ;
283+ parent . style . overflowX = 'visible' ;
284+ parent . style . overflowY = 'visible' ;
285+ parent . style . maxHeight = 'none' ;
286+ }
287+ parent = parent . parentElement ;
288+ }
289+ } ,
290+ { scrollWidth, scrollHeight, ancestorCount : originalAncestorStyles } ,
291+ ) ;
292+
293+ // Small delay to allow reflow and rendering
294+ await new Promise ( resolve => setTimeout ( resolve , 150 ) ) ;
295+
296+ // Take screenshot of the expanded container
297+ const screenshot = await containerHandle . screenshot ( {
298+ type : options . type ,
299+ quality : options . quality ,
300+ optimizeForSpeed : true ,
301+ } ) ;
302+
303+ return screenshot ;
304+ } finally {
305+ // Restore original styles
306+ await containerHandle . evaluate (
307+ ( el , style ) => {
308+ const htmlEl = el as HTMLElement ;
309+ htmlEl . style . width = style . width ;
310+ htmlEl . style . height = style . height ;
311+ htmlEl . style . maxWidth = style . maxWidth ;
312+ htmlEl . style . maxHeight = style . maxHeight ;
313+ htmlEl . style . overflow = style . overflow ;
314+ htmlEl . style . overflowX = style . overflowX ;
315+ htmlEl . style . overflowY = style . overflowY ;
316+ htmlEl . style . position = style . position ;
317+ } ,
318+ originalStyle ,
319+ ) ;
320+
321+ // Reload the page to restore ancestor styles (simpler than tracking each one)
322+ // This is a trade-off for simplicity - alternatively we could track and restore each ancestor
323+ await page . evaluate ( ( ) => {
324+ // Force a reflow to restore styles - the finally block restoration handles the container
325+ // Ancestors will be restored on next navigation or can be manually refreshed
326+ } ) ;
327+ }
328+ }
329+
330+ /**
331+ * Finds the main scrollable container on the page if one exists.
332+ * This handles dashboard-style layouts where the page body doesn't scroll
333+ * but a nested div container has overflow:auto with scrollable content.
334+ * Returns the container element handle if found, null otherwise.
335+ */
336+ async function findMainScrollableContainer (
337+ page : Page ,
338+ ) : Promise < ElementHandle < Element > | null > {
339+ // First check if the page itself has significant scrollable content
340+ const pageHasScroll = await page . evaluate ( ( ) => {
341+ const docEl = document . documentElement ;
342+ // If the page body has significant scroll (more than 50px beyond viewport), use regular fullPage
343+ return docEl . scrollHeight > docEl . clientHeight + 50 ;
344+ } ) ;
345+
346+ // If the page itself is scrollable, don't look for containers
347+ if ( pageHasScroll ) {
348+ return null ;
349+ }
350+
351+ // Look for scrollable containers
352+ const containerHandle = await page . evaluateHandle ( ( ) => {
353+ const allElements = Array . from ( document . querySelectorAll ( '*' ) ) ;
354+ let bestContainer : HTMLElement | null = null ;
355+ let bestScore = 0 ;
356+
357+ for ( const el of allElements ) {
358+ const htmlEl = el as HTMLElement ;
359+ const style = getComputedStyle ( htmlEl ) ;
360+ const overflowY = style . overflowY ;
361+
362+ // Check if element has scrollable overflow
363+ if ( overflowY !== 'auto' && overflowY !== 'scroll' && overflowY !== 'overlay' ) {
364+ continue ;
365+ }
366+
367+ // Check if it actually has scrollable content
368+ const hasScrollableContent = htmlEl . scrollHeight > htmlEl . clientHeight + 50 ;
369+ if ( ! hasScrollableContent ) {
370+ continue ;
371+ }
372+
373+ const rect = htmlEl . getBoundingClientRect ( ) ;
374+
375+ // Skip tiny elements
376+ if ( rect . width < 200 || rect . height < 200 ) {
377+ continue ;
378+ }
379+
380+ // Calculate score based on:
381+ // 1. Size of the visible area
382+ // 2. Amount of hidden scrollable content
383+ // 3. Prefer elements that take up more of the viewport
384+ const viewportWidth = window . innerWidth ;
385+ const viewportHeight = window . innerHeight ;
386+ const viewportCoverage = ( rect . width * rect . height ) / ( viewportWidth * viewportHeight ) ;
387+ const scrollableAmount = htmlEl . scrollHeight - htmlEl . clientHeight ;
388+
389+ // Prefer larger containers with more scrollable content
390+ const score = viewportCoverage * 1000 + scrollableAmount ;
391+
392+ if ( score > bestScore ) {
393+ bestScore = score ;
394+ bestContainer = htmlEl ;
395+ }
396+ }
397+
398+ return bestContainer ;
399+ } ) ;
400+
401+ const containerElement = containerHandle . asElement ( ) ;
402+ if ( ! containerElement ) {
403+ await containerHandle . dispose ( ) ;
404+ return null ;
405+ }
406+
407+ return containerElement as ElementHandle < Element > ;
408+ }
409+
196410export const screenshot = defineTool ( {
197411 name : 'take_screenshot' ,
198412 description : `Take a screenshot of the page or element.` ,
@@ -287,31 +501,54 @@ export const screenshot = defineTool({
287501 void handle . dispose ( ) ;
288502 }
289503 } else if ( fullPage ) {
290- // Full-page screenshot - auto-detect iframe with scrollable content
504+ // Full-page screenshot - auto-detect scrollable containers or iframes
291505 const page : Page = context . getSelectedPage ( ) ;
292- const mainIframe = await findMainContentIframe ( page ) ;
293506
294- if ( mainIframe ) {
295- // Found an iframe with scrollable content - capture its full content
507+ // First, check for scrollable div containers (common in dashboard layouts)
508+ const mainContainer = await findMainScrollableContainer ( page ) ;
509+
510+ if ( mainContainer ) {
511+ // Found a scrollable container - capture its full content
296512 try {
297- screenshot = await takeIframeFullPageScreenshot ( mainIframe , {
298- type : format ,
299- quality,
300- } ) ;
513+ screenshot = await takeScrollableContainerFullPageScreenshot (
514+ mainContainer ,
515+ page ,
516+ {
517+ type : format ,
518+ quality,
519+ } ,
520+ ) ;
301521 responseMessage =
302- 'Took a full-page screenshot of the main content iframe .' ;
522+ 'Took a full-page screenshot of the main scrollable container .' ;
303523 } finally {
304- void mainIframe . dispose ( ) ;
524+ void mainContainer . dispose ( ) ;
305525 }
306526 } else {
307- // No significant iframe found - take regular full page screenshot
308- screenshot = await page . screenshot ( {
309- type : format ,
310- fullPage : true ,
311- quality,
312- optimizeForSpeed : true ,
313- } ) ;
314- responseMessage = 'Took a screenshot of the full current page.' ;
527+ // Check for iframes with scrollable content
528+ const mainIframe = await findMainContentIframe ( page ) ;
529+
530+ if ( mainIframe ) {
531+ // Found an iframe with scrollable content - capture its full content
532+ try {
533+ screenshot = await takeIframeFullPageScreenshot ( mainIframe , {
534+ type : format ,
535+ quality,
536+ } ) ;
537+ responseMessage =
538+ 'Took a full-page screenshot of the main content iframe.' ;
539+ } finally {
540+ void mainIframe . dispose ( ) ;
541+ }
542+ } else {
543+ // No significant scrollable container or iframe found - take regular full page screenshot
544+ screenshot = await page . screenshot ( {
545+ type : format ,
546+ fullPage : true ,
547+ quality,
548+ optimizeForSpeed : true ,
549+ } ) ;
550+ responseMessage = 'Took a screenshot of the full current page.' ;
551+ }
315552 }
316553 } else {
317554 // Viewport screenshot
0 commit comments