@@ -597,6 +597,24 @@ func activate(pid: pid_t) {
597597 }
598598}
599599
600+ /// activate + polling 直到目标 PID 变成 frontmostApplication,或 timeoutMs 超时。
601+ /// macOS 14+ Stage Manager / 焦点窃取保护下,NSWorkspace.activate 是异步的——立刻调用
602+ /// validate_geometry 会看到 is_foreground=false,给上层误判。这里同步等到 NSWorkspace
603+ /// 真切到目标 PID 才返回。
604+ /// 返回 (success, frontmost_pid_at_exit)。success=false 时上层应直接抛 error。
605+ func activateAndWaitForeground( pid: pid_t , timeoutMs: Int = 1500 ) -> ( Bool , pid_t ) {
606+ activate ( pid: pid)
607+ let deadline = Date ( ) . addingTimeInterval ( Double ( timeoutMs) / 1000.0 )
608+ var frontPid : pid_t = NSWorkspace . shared. frontmostApplication? . processIdentifier ?? 0
609+ if frontPid == pid { return ( true , frontPid) }
610+ while Date ( ) < deadline {
611+ usleep ( 60_000 ) // 60ms
612+ frontPid = NSWorkspace . shared. frontmostApplication? . processIdentifier ?? 0
613+ if frontPid == pid { return ( true , frontPid) }
614+ }
615+ return ( false , frontPid)
616+ }
617+
600618func moveWindow( handle: String , rect: CGRect ) -> [ String : Any ] ? {
601619 guard let ( _, axWin, _) = findWindow ( handle: handle) else { return nil }
602620 if axBoolAttr ( axWin, kAXMinimizedAttribute) == true {
@@ -609,8 +627,8 @@ func moveWindow(handle: String, rect: CGRect) -> [String: Any]? {
609627 AXUIElementSetAttributeValue ( axWin, kAXPositionAttribute as CFString , posVal)
610628 AXUIElementSetAttributeValue ( axWin, kAXSizeAttribute as CFString , sizeVal)
611629 let pid = pid_t ( handle. split ( separator: " : " ) . first!) !
612- activate ( pid : pid )
613- usleep ( 120_000 )
630+ // 同步等到 PID 真切前台。120ms 在 macOS 14+ 焦点窃取保护下经常不够。
631+ _ = activateAndWaitForeground ( pid : pid )
614632 if let ( _, _, desc) = findWindow ( handle: handle) {
615633 return toWindowInfo ( desc)
616634 }
@@ -926,14 +944,29 @@ func handle(method: String, params: [String: Any]) -> Any {
926944 case " window.activate " :
927945 guard let handle = params [ " handle " ] as? String ,
928946 let pid = pid_t ( handle. split ( separator: " : " ) . first. map ( String . init) ?? " " ) else { return [ " error " : " bad handle " ] }
929- activate ( pid: pid)
930- return [ " ok " : true ]
947+ let ( ok, frontPid) = activateAndWaitForeground ( pid: pid)
948+ if ok { return [ " ok " : true , " frontmost_pid " : Int ( frontPid) ] }
949+ // 超时——给上层提供详细诊断让 agent 知道为什么 raise 失败
950+ return [
951+ " ok " : false ,
952+ " reason " : " foreground_timeout " ,
953+ " target_pid " : Int ( pid) ,
954+ " frontmost_pid " : Int ( frontPid) ,
955+ " hint " : " macOS 焦点窃取保护拦截了 activation;调用者可能持续 frontmost。让用户手动 cmd+tab 到目标 app,或在 Claude Code 中先点击桌面再重试 "
956+ ]
931957 case " window.raise " :
932958 // window.raise 等价于 window.activate(保留向后兼容)
933959 guard let handle = params [ " handle " ] as? String ,
934960 let pid = pid_t ( handle. split ( separator: " : " ) . first. map ( String . init) ?? " " ) else { return [ " error " : " bad handle " ] }
935- activate ( pid: pid)
936- return [ " ok " : true ]
961+ let ( ok, frontPid) = activateAndWaitForeground ( pid: pid)
962+ if ok { return [ " ok " : true , " frontmost_pid " : Int ( frontPid) ] }
963+ return [
964+ " ok " : false ,
965+ " reason " : " foreground_timeout " ,
966+ " target_pid " : Int ( pid) ,
967+ " frontmost_pid " : Int ( frontPid) ,
968+ " hint " : " macOS 焦点窃取保护拦截了 activation;调用者可能持续 frontmost。让用户手动 cmd+tab 到目标 app,或在 Claude Code 中先点击桌面再重试 "
969+ ]
937970 case " ax.dump " :
938971 guard let handle = params [ " handle " ] as? String else { return [ " error " : " handle required " ] }
939972 let maxNodes = ( params [ " max_nodes " ] as? NSNumber ) ? . intValue ?? 500
0 commit comments