Inconsistent XML persistence with project-level PersistentStateComponent in split mode

Hi,

I have some issue with handling project-level PersistentStateComponents in split mode.

Currently, I have two modules for implementing the settings, settings-shared and settings-frontend with descriptors and classes like below.

When I first open the plugin’s settings UI, the checkbox states are set correctly according to the default state values.

  • When I change them the first time (from default state to non-default), the corresponding settings XML is created in the project’s .idea folder with the correct, up-to-date state.
  • The opposite is true too, that when the settings change from non-default to default, the settings XML gets deleted from the .idea folder.

However, when I change the settings from a non-default state to another non-default state, the settings XML doesn’t get updated at all with the new state, only the UI part.

I’m kind of clueless as to what I might be missing.

  • I have the <projectSettings> and <rdct.remoteSettingProvider> registered for syncing the settings, which is working to a certain extent because the settings XML gets created and removed in the mentioned cases.
  • I tried switching from light service to <projectService> registration. No difference.
  • I tried having the rdct.remoteSettingProvider in the settings-shared module only, or in settings-frontend and a backend module that uses the settings, but that didn’t make any difference.
  • I also tried using Direction.InitialFromFrontend instead of Direction.InitialFromBackend, just in case. No difference.
  • I even tried with a minimal project setup that contains only the settings-shared and settings-frontend modules to see if anything else interferes with it. No difference.

Has anyone else encountered this issue before?

Environment

I could reproduce it in a split-mode sandbox IDE and in a VirtualBox (as remote) + JetBrains Gateway setup where I install the plugin via the Install plugin from disk options.

I build on IJ 2026.2 with platform plugin 2.18.1.

Plugin configuration

settings-shared.xml

<idea-plugin>  
  <extensions defaultExtensionNs="com.intellij">  
    <projectSettings service="settings.MySettings"/>  
    <rdct.remoteSettingProvider 
       implementation="settings.MySettingsRemoteInfoProvider"/>  
  </extensions>
</idea-plugin>

settings-frontend.xml

<idea-plugin>  
  <dependencies>
    <module name="intellij.platform.frontend"/>  
    <module name="settings-shared"/>  
  </dependencies>
  
  <resource-bundle>messages.MySettingsBundle</resource-bundle>  
  
  <extensions defaultExtensionNs="com.intellij">  
    <projectConfigurable id="MyConfigurable" 
                         instance="settings.MyConfigurable"
                         groupId="tools"
                         key="my.configurable"/>  
  </extensions>  
</idea-plugin>

PersistentStateComponent

@State(name = "MySettings", storages = [Storage(value = "mySettings.xml")])  
@Service(Service.Level.PROJECT)  
class MySettings(private val project: Project) : PersistentStateComponent<MyState> {  
  
  var isFirstOption: Boolean  
    get() = state.isFirstOption  
    set(value) { state.isFirstOption = value }  
  
  var isSecondOption: Boolean  
    get() = state.isSecondOption  
    set(value) { state.isSecondOption = value }  
  
  override fun noStateLoaded() {  
    loadState(MyState())
  }  
}  
  
class MyState {  
  var isFirstOption: Boolean = true
  var isSecondOption: Boolean = false
}

RemoteSettingInfoProvider

class MySettingsRemoteInfoProvider : RemoteSettingInfoProvider {  
  override fun getRemoteSettingsInfo() = mapOf(  
    "MySettings" to RemoteSettingInfo(direction = RemoteSettingInfo.Direction.InitialFromBackend)  
  )  
}

Configurable

internal class MyConfigurable(private val project: Project) : Configurable {
	//...
	
	override fun createComponent(): JComponent {
		val settings = project.service<MySettings>()
		panel = panel {  
	      row {  
	        checkBox("First option").bindSelected(settings::isFirstOption)  
	      }  
	  
	      row {  
	        checkBox("Second option").bindSelected(settings::isSecondOption)  
	      }  
	    }  
	    return panel
	}
}