@@ -295,14 +295,34 @@ impl ComputerUseLinux {
295295 let max_depth = params. max_depth . unwrap_or ( 12 ) . min ( 12 ) ;
296296 let include_screenshot = params. include_screenshot . unwrap_or ( true ) ;
297297 let screenshot_options = params. screenshot_options ( ) ;
298+ let screenshot_target_requested = params. window_target ( ) . has_target ( ) ;
298299 let app_filter = self
299300 . resolve_accessibility_app_filter ( & params, window_context. as_ref ( ) )
300301 . await ;
301302 let ( screenshot, screenshot_error) = if include_screenshot {
302- match capture_screenshot_raw ( )
303- . await
304- . and_then ( |raw| prepare_screenshot_payload ( raw, screenshot_options) )
305- {
303+ let result = capture_screenshot_raw ( ) . await . and_then ( |raw| {
304+ if let Some ( window) = window_context. as_ref ( ) {
305+ let bounds = window. bounds . as_ref ( ) . ok_or_else ( || {
306+ anyhow:: anyhow!(
307+ "targeted screenshot requires window bounds; refusing to return the full desktop"
308+ )
309+ } ) ?;
310+ prepare_app_state_screenshot (
311+ raw,
312+ Some ( bounds) ,
313+ screenshot_target_requested,
314+ screenshot_options,
315+ )
316+ } else {
317+ prepare_app_state_screenshot (
318+ raw,
319+ None ,
320+ screenshot_target_requested,
321+ screenshot_options,
322+ )
323+ }
324+ } ) ;
325+ match result {
306326 Ok ( capture) => ( Some ( capture) , None ) ,
307327 Err ( error) => ( None , Some ( format ! ( "{error:#}" ) ) ) ,
308328 }
@@ -1934,9 +1954,9 @@ struct ActionOutput {
19341954impl ComputerUseLinux {
19351955 fn is_wayland_session ( & self ) -> bool {
19361956 crate :: diagnostics:: hydrate_session_bus_env ( ) ;
1937- env:: var ( "XDG_SESSION_TYPE" )
1938- . ok ( )
1939- . is_some_and ( |value| value . eq_ignore_ascii_case ( "wayland" ) )
1957+ let session_type = env:: var ( "XDG_SESSION_TYPE" ) . ok ( ) ;
1958+ let wayland_display = env :: var ( "WAYLAND_DISPLAY" ) . ok ( ) ;
1959+ session_is_wayland ( session_type . as_deref ( ) , wayland_display . as_deref ( ) )
19401960 }
19411961
19421962 // The Wayland remote-desktop portal is now a *fallback* for input: when a
@@ -2975,6 +2995,56 @@ fn data_url_payload(data_url: &str) -> String {
29752995 . to_string ( )
29762996}
29772997
2998+ fn session_is_wayland ( session_type : Option < & str > , wayland_display : Option < & str > ) -> bool {
2999+ match session_type {
3000+ Some ( value) => value. eq_ignore_ascii_case ( "wayland" ) ,
3001+ None => wayland_display. is_some_and ( |value| !value. is_empty ( ) ) ,
3002+ }
3003+ }
3004+
3005+ fn prepare_app_state_screenshot (
3006+ mut raw : RawScreenshotCapture ,
3007+ bounds : Option < & crate :: windowing:: WindowBounds > ,
3008+ target_requested : bool ,
3009+ options : ScreenshotPayloadOptions ,
3010+ ) -> Result < ScreenshotCapture > {
3011+ if target_requested && bounds. is_none ( ) {
3012+ anyhow:: bail!(
3013+ "targeted screenshot requires a resolved window; refusing to return the full desktop"
3014+ ) ;
3015+ }
3016+ if let Some ( bounds) = bounds {
3017+ let ( mut x, mut y, mut width, mut height) = window_crop_rect ( bounds) . ok_or_else ( || {
3018+ anyhow:: anyhow!(
3019+ "targeted screenshot has unusable window bounds; refusing to return the full desktop"
3020+ )
3021+ } ) ?;
3022+ if x < 0 {
3023+ width = width. saturating_sub ( x. unsigned_abs ( ) ) ;
3024+ x = 0 ;
3025+ }
3026+ if y < 0 {
3027+ height = height. saturating_sub ( y. unsigned_abs ( ) ) ;
3028+ y = 0 ;
3029+ }
3030+ if width == 0 || height == 0 {
3031+ anyhow:: bail!(
3032+ "targeted screenshot window is outside the captured desktop; refusing to return the full desktop"
3033+ ) ;
3034+ }
3035+ let ( bytes, width, height) = crop_png ( & raw . bytes, x, y, width, height)
3036+ . map_err ( |error| anyhow:: anyhow!( "targeted screenshot crop failed: {error}" ) ) ?;
3037+ raw = RawScreenshotCapture {
3038+ mime_type : raw. mime_type ,
3039+ bytes,
3040+ source : raw. source ,
3041+ width,
3042+ height,
3043+ } ;
3044+ }
3045+ prepare_screenshot_payload ( raw, options)
3046+ }
3047+
29783048/// Convert a window's bounds into a crop rectangle, if it has a usable origin
29793049/// and non-zero size.
29803050fn window_crop_rect ( bounds : & crate :: windowing:: WindowBounds ) -> Option < ( i32 , i32 , u32 , u32 ) > {
@@ -3876,6 +3946,95 @@ mod tests {
38763946 out
38773947 }
38783948
3949+ #[ test]
3950+ fn targeted_app_state_crops_before_screenshot_payload_resize ( ) {
3951+ let raw = RawScreenshotCapture {
3952+ mime_type : "image/png" . to_string ( ) ,
3953+ bytes : solid_png ( 400 , 200 ) ,
3954+ source : "test" . to_string ( ) ,
3955+ width : 400 ,
3956+ height : 200 ,
3957+ } ;
3958+ let bounds = WindowBounds {
3959+ x : Some ( 50 ) ,
3960+ y : Some ( 20 ) ,
3961+ width : 200 ,
3962+ height : 100 ,
3963+ } ;
3964+ let capture = prepare_app_state_screenshot (
3965+ raw,
3966+ Some ( & bounds) ,
3967+ true ,
3968+ ScreenshotPayloadOptions {
3969+ max_width : Some ( 100 ) ,
3970+ max_height : Some ( 100 ) ,
3971+ max_bytes : Some ( 1024 * 1024 ) ,
3972+ ..Default :: default ( )
3973+ } ,
3974+ )
3975+ . unwrap ( ) ;
3976+
3977+ assert_eq ! (
3978+ ( capture. coordinate_width, capture. coordinate_height) ,
3979+ ( 200 , 100 )
3980+ ) ;
3981+ assert_eq ! ( ( capture. width, capture. height) , ( 100 , 50 ) ) ;
3982+ }
3983+
3984+ #[ test]
3985+ fn unresolved_app_state_target_refuses_full_desktop_screenshot ( ) {
3986+ let raw = RawScreenshotCapture {
3987+ mime_type : "image/png" . to_string ( ) ,
3988+ bytes : solid_png ( 400 , 200 ) ,
3989+ source : "test" . to_string ( ) ,
3990+ width : 400 ,
3991+ height : 200 ,
3992+ } ;
3993+
3994+ let error =
3995+ prepare_app_state_screenshot ( raw, None , true , ScreenshotPayloadOptions :: default ( ) )
3996+ . unwrap_err ( ) ;
3997+
3998+ assert ! ( error. to_string( ) . contains( "requires a resolved window" ) ) ;
3999+ }
4000+
4001+ #[ test]
4002+ fn targeted_app_state_crops_only_visible_part_of_offscreen_window ( ) {
4003+ let raw = RawScreenshotCapture {
4004+ mime_type : "image/png" . to_string ( ) ,
4005+ bytes : solid_png ( 400 , 200 ) ,
4006+ source : "test" . to_string ( ) ,
4007+ width : 400 ,
4008+ height : 200 ,
4009+ } ;
4010+ let bounds = WindowBounds {
4011+ x : Some ( -50 ) ,
4012+ y : Some ( -40 ) ,
4013+ width : 100 ,
4014+ height : 100 ,
4015+ } ;
4016+
4017+ let capture = prepare_app_state_screenshot (
4018+ raw,
4019+ Some ( & bounds) ,
4020+ true ,
4021+ ScreenshotPayloadOptions :: default ( ) ,
4022+ )
4023+ . unwrap ( ) ;
4024+
4025+ assert_eq ! (
4026+ ( capture. coordinate_width, capture. coordinate_height) ,
4027+ ( 50 , 60 )
4028+ ) ;
4029+ }
4030+
4031+ #[ test]
4032+ fn wayland_display_is_enough_to_select_portal_fallback ( ) {
4033+ assert ! ( session_is_wayland( None , Some ( "wayland-1" ) ) ) ;
4034+ assert ! ( session_is_wayland( Some ( "wayland" ) , None ) ) ;
4035+ assert ! ( !session_is_wayland( Some ( "x11" ) , None ) ) ;
4036+ }
4037+
38794038 #[ test]
38804039 fn window_crop_happens_before_screenshot_payload_resize ( ) {
38814040 let ( cropped, width, height) = crop_png ( & solid_png ( 400 , 200 ) , 50 , 20 , 200 , 100 ) . unwrap ( ) ;
0 commit comments