11import React , { useCallback , useEffect , useLayoutEffect , useRef , useState } from 'react' ;
22import { useTranslation } from 'react-i18next' ;
33import { listen } from '@tauri-apps/api/event' ;
4- import { getCurrentWindow } from '@tauri-apps/api/window' ;
4+ import { cursorPosition , getCurrentWindow } from '@tauri-apps/api/window' ;
55import { aiExperienceConfigService , type AgentCompanionPetSelection , type AIExperienceSettings } from '@/infrastructure/config/services/AIExperienceConfigService' ;
66import { ChatInputPixelPet , type ChatInputPixelPetMood } from '@/flow_chat/components/ChatInputPixelPet' ;
77import type { ChatInputPetMood } from '@/flow_chat/utils/chatInputPetMood' ;
@@ -21,6 +21,7 @@ const BUBBLE_GAP = 6;
2121const BUBBLE_MIN_WIDTH = 132 ;
2222const BUBBLE_MAX_WIDTH = 252 ;
2323const WINDOW_EDGE_BUFFER = 4 ;
24+ const POINTER_HOVER_POLL_INTERVAL_MS = 120 ;
2425
2526export const AgentCompanionDesktopPet : React . FC = ( ) => {
2627 const { t } = useTranslation ( 'flow-chat' ) ;
@@ -144,6 +145,98 @@ export const AgentCompanionDesktopPet: React.FC = () => {
144145 } ) ;
145146 } , [ activePetSize . height , activePetSize . width , tasks ] ) ;
146147
148+ useEffect ( ( ) => {
149+ const tauriWindow = getCurrentWindow ( ) ;
150+ let disposed = false ;
151+ let windowPosition : { x : number ; y : number } | null = null ;
152+ let scaleFactor = 1 ;
153+ let removeWindowMovedListener : ( ( ) => void ) | null = null ;
154+ let removeScaleChangedListener : ( ( ) => void ) | null = null ;
155+
156+ void tauriWindow . outerPosition ( )
157+ . then ( position => {
158+ windowPosition = position ;
159+ } )
160+ . catch ( error => {
161+ log . warn ( 'Failed to read Agent companion window position' , error ) ;
162+ } ) ;
163+
164+ void tauriWindow . scaleFactor ( )
165+ . then ( nextScaleFactor => {
166+ scaleFactor = nextScaleFactor ;
167+ } )
168+ . catch ( error => {
169+ log . warn ( 'Failed to read Agent companion window scale factor' , error ) ;
170+ } ) ;
171+
172+ void tauriWindow . onMoved ( event => {
173+ windowPosition = event . payload ;
174+ } ) . then ( unlisten => {
175+ if ( disposed ) {
176+ unlisten ( ) ;
177+ } else {
178+ removeWindowMovedListener = unlisten ;
179+ }
180+ } ) . catch ( error => {
181+ log . warn ( 'Failed to listen for Agent companion window moves' , error ) ;
182+ } ) ;
183+
184+ void tauriWindow . onScaleChanged ( event => {
185+ scaleFactor = event . payload . scaleFactor ;
186+ } ) . then ( unlisten => {
187+ if ( disposed ) {
188+ unlisten ( ) ;
189+ } else {
190+ removeScaleChangedListener = unlisten ;
191+ }
192+ } ) . catch ( error => {
193+ log . warn ( 'Failed to listen for Agent companion scale changes' , error ) ;
194+ } ) ;
195+
196+ const pollPointerHover = async ( ) => {
197+ try {
198+ if ( ! windowPosition ) {
199+ windowPosition = await tauriWindow . outerPosition ( ) ;
200+ }
201+
202+ const pointer = await cursorPosition ( ) ;
203+ if ( disposed ) {
204+ return ;
205+ }
206+
207+ const hitbox = dockRef . current ?. querySelector < HTMLElement > ( '.bitfun-agent-companion-window__pet-hitbox' ) ;
208+ if ( ! hitbox ) {
209+ setIsHoveringPet ( false ) ;
210+ return ;
211+ }
212+
213+ const hitboxRect = hitbox . getBoundingClientRect ( ) ;
214+ const pointerX = ( pointer . x - windowPosition . x ) / scaleFactor ;
215+ const pointerY = ( pointer . y - windowPosition . y ) / scaleFactor ;
216+ const isPointerInsideHitbox = pointerX >= hitboxRect . left
217+ && pointerX <= hitboxRect . right
218+ && pointerY >= hitboxRect . top
219+ && pointerY <= hitboxRect . bottom ;
220+
221+ setIsHoveringPet ( isPointerInsideHitbox ) ;
222+ } catch ( error ) {
223+ log . warn ( 'Failed to poll Agent companion pointer hover state' , error ) ;
224+ }
225+ } ;
226+
227+ const intervalId = window . setInterval ( ( ) => {
228+ void pollPointerHover ( ) ;
229+ } , POINTER_HOVER_POLL_INTERVAL_MS ) ;
230+ void pollPointerHover ( ) ;
231+
232+ return ( ) => {
233+ disposed = true ;
234+ window . clearInterval ( intervalId ) ;
235+ removeWindowMovedListener ?.( ) ;
236+ removeScaleChangedListener ?.( ) ;
237+ } ;
238+ } , [ ] ) ;
239+
147240 const startDrag = ( event : React . PointerEvent < HTMLDivElement > ) => {
148241 if ( event . button !== 0 ) {
149242 return ;
0 commit comments