diff --git a/electron/package.json b/electron/package.json index f9cbe838..909a4472 100644 --- a/electron/package.json +++ b/electron/package.json @@ -1,6 +1,6 @@ { "name": "openswarm", - "version": "1.1.61", + "version": "1.1.62", "description": "OpenSwarm — AI Agent Orchestrator", "author": "openswarm-ai", "main": "main.js", diff --git a/frontend/src/app/components/Onboarding/_motionWin.tsx b/frontend/src/app/components/Onboarding/_motionWin.tsx index e99c7ee7..882f137c 100644 --- a/frontend/src/app/components/Onboarding/_motionWin.tsx +++ b/frontend/src/app/components/Onboarding/_motionWin.tsx @@ -24,11 +24,29 @@ const stripFramerProps = (props: any) => { return out; }; +// Components that drive position via `animate={{ x, y }}` (ACPopup, ACMultiChoice, etc.) would otherwise lose their layout when the animate prop is stripped, because they have no fallback style.transform. We salvage the latest numeric x/y from animate and apply them as a transform so the div lands in the right place; no animation, just static placement. const motionShim: any = new Proxy({}, { get: (_target, tag: string) => { - return React.forwardRef((props: any, ref: any) => - React.createElement(tag, { ...stripFramerProps(props), ref }) - ); + return React.forwardRef((props: any, ref: any) => { + let translate = ''; + const a = props.animate; + if (a && typeof a === 'object' && !Array.isArray(a)) { + const ax = typeof a.x === 'number' ? a.x : null; + const ay = typeof a.y === 'number' ? a.y : null; + if (ax !== null || ay !== null) { + translate = `translate(${ax ?? 0}px, ${ay ?? 0}px)`; + } + } + const stripped = stripFramerProps(props); + if (translate) { + const existing = stripped.style && stripped.style.transform; + stripped.style = { + ...(stripped.style || {}), + transform: existing ? `${existing} ${translate}` : translate, + }; + } + return React.createElement(tag, { ...stripped, ref }); + }); }, }); diff --git a/frontend/src/app/components/Onboarding/ac/AgenticCursor.tsx b/frontend/src/app/components/Onboarding/ac/AgenticCursor.tsx index d2134dd8..67cd7c09 100644 --- a/frontend/src/app/components/Onboarding/ac/AgenticCursor.tsx +++ b/frontend/src/app/components/Onboarding/ac/AgenticCursor.tsx @@ -8,7 +8,7 @@ import React, { import { createPortal } from 'react-dom'; import { motion, useAnimationControls, AnimatePresence } from '../_motionWin'; import { useClaudeTokens } from '@/shared/styles/ThemeContext'; -import { cursorStore } from './cursorStore'; +import { cursorStore, useCursorPosition } from './cursorStore'; import { resolveSelector } from '../selectors'; import ACPopup from './ACPopup'; import ACMultiChoice from './ACMultiChoice'; @@ -53,9 +53,13 @@ interface MultiChoiceState { // Snappy 260/26 spring; calm comes from popup cadence + 3s dwell, not cursor delay. const SPRING = { type: 'spring' as const, stiffness: 260, damping: 26 }; +// On Windows the motionWin shim strips Framer Motion's animate prop, so controls.set({x,y}) never moves the wrapper. We bypass by reading the same store the popups read and applying style.transform directly; Mac is unaffected since Framer's own transform writes win the cascade. +const IS_WIN = typeof navigator !== 'undefined' && navigator.userAgent.includes('Windows'); + const AgenticCursor = forwardRef((_props, ref) => { const c = useClaudeTokens(); const controls = useAnimationControls(); + const storePos = useCursorPosition(); const posRef = useRef({ x: 0, y: 0 }); const [visible, setVisible] = useState(false); const [popup, setPopup] = useState(null); @@ -255,6 +259,7 @@ const AgenticCursor = forwardRef((_props, ref) => { zIndex: 10500, pointerEvents: 'none', transformOrigin: 'top left', + ...(IS_WIN ? { transform: `translate(${storePos.x}px, ${storePos.y}px)` } : null), }} > {visible && (