@@ -23,18 +23,21 @@ import java.util.concurrent.atomic.AtomicReference
2323class OpenCodeSettingsConfigurable (private val project : Project ) :
2424 BoundConfigurable (" OpenCode Companion" ) {
2525
26+ private val pendingState = OpenCodeSettings .State ()
27+
2628 internal var executableResolver: (String? ) -> OpenCodeInfo ? = { path -> OpenCodeChecker .findExecutable(path) }
2729
2830 override fun createPanel (): DialogPanel {
29- val settings = OpenCodeSettings .getInstance(project)
31+ loadPendingFromPersisted()
32+
3033 return panel {
3134 group(" Executable" ) {
3235 val executablePathField = TextFieldWithBrowseButton ().apply {
3336 addBrowseFolderListener(
3437 project,
3538 FileChooserDescriptorFactory .createSingleFileNoJarsDescriptor()
3639 .withTitle(" Select OpenCode Executable" )
37- .withDescription(" Choose the opencode executable file." )
40+ .withDescription(" Choose the opencode executable file." ),
3841 )
3942 }
4043 run {
@@ -57,33 +60,33 @@ class OpenCodeSettingsConfigurable(private val project: Project) :
5760 }
5861 row(" OpenCode Path:" ) {
5962 cell(executablePathField)
60- .bindText(settings ::executablePath)
63+ .bindText(pendingState ::executablePath)
6164 .comment(" Path to the opencode executable. Leave blank to auto-detect." )
6265 .align(AlignX .FILL )
6366 }
6467 }
6568 group(" Server" ) {
6669 row(" Server Port:" ) {
6770 intTextField(1024 .. 65535 )
68- .bindIntText(settings ::serverPort)
71+ .bindIntText(pendingState ::serverPort)
6972 .comment(" Port the OpenCode server listens on (default: 4096)" )
7073 }
7174 }
7275 group(" Editor" ) {
7376 row {
7477 checkBox(" Show inline diff highlights" )
75- .bindSelected(settings ::inlineDiffEnabled)
78+ .bindSelected(pendingState ::inlineDiffEnabled)
7679 .comment(
7780 " Renders green/red inline diff highlights in the editor " +
78- " for AI-modified files. Changes take effect immediately."
81+ " for AI-modified files. Changes take effect immediately." ,
7982 )
8083 }
8184 }
8285 group(" Terminal" ) {
8386 val reworkedSupported = BuildUtils .isEmbeddedTerminalSupported
8487 row {
8588 checkBox(" Show inline terminal" )
86- .bindSelected(settings ::inlineTerminalEnabled)
89+ .bindSelected(pendingState ::inlineTerminalEnabled)
8790 .comment(" Embeds the OpenCode TUI directly inside the tool window panel when the server is running." )
8891 }
8992 buttonsGroup(" Terminal engine:" ) {
@@ -96,138 +99,141 @@ class OpenCodeSettingsConfigurable(private val project: Project) :
9699 .enabled(reworkedSupported)
97100 .comment(
98101 if (reworkedSupported) " New terminal engine (IntelliJ 2025.3+)."
99- else " Requires IntelliJ 2025.3 or later."
102+ else " Requires IntelliJ 2025.3 or later." ,
100103 )
101104 }
102- }.bind(settings ::terminalEngine)
105+ }.bind(pendingState ::terminalEngine)
103106 }
104107 group(" Diagnostics" ) {
105108 row {
106109 checkBox(" Enable diff trace logging" )
107- .bindSelected(settings ::diffTraceEnabled)
110+ .bindSelected(pendingState ::diffTraceEnabled)
108111 .comment(
109112 " Writes a JSONL trace file to the system temp directory " +
110113 " (opencode-diff-traces/) for debugging diff pipeline events. " +
111- " Takes effect after restarting the IDE."
114+ " Takes effect after restarting the IDE." ,
112115 )
113116 }
114117 row {
115118 checkBox(" Include historical diffs in trace" )
116- .bindSelected(settings ::diffTraceHistoryEnabled)
119+ .bindSelected(pendingState ::diffTraceHistoryEnabled)
117120 .comment(
118121 " Also records events from historical (loaded-on-demand) session diffs " +
119122 " in the trace. Only relevant when diff trace logging is enabled. " +
120- " Takes effect after restarting the IDE."
123+ " Takes effect after restarting the IDE." ,
121124 )
122125 }
123126 }
124127 }
125128 }
126129
130+ override fun reset () {
131+ loadPendingFromPersisted()
132+ super .reset()
133+ }
134+
127135 override fun apply () {
128136 val settings = OpenCodeSettings .getInstance(project)
129137 val plugin = OpenCodePlugin .getInstance(project)
130- val oldSettings = snapshot(settings)
138+ val oldSettings = snapshot(settings.state )
131139 val oldOpenCodeInfo = plugin.openCodeInfo
132140
133- super .apply ()
141+ super .apply () // Pushes UI values into pendingState.
134142
135- val newSettings = snapshot(settings)
143+ val newSettings = snapshot(pendingState)
144+ val settingsChanged = newSettings != oldSettings
136145 val newPort = newSettings.serverPort
137146 val newPath = newSettings.executablePath
138147 val portChanged = newPort != oldSettings.serverPort
139148 val pathChanged = newPath != oldSettings.executablePath
140149 val shouldResolveInfo = pathChanged || (newPath.isBlank() && oldOpenCodeInfo == null )
141- val portOrPathChanged = portChanged || pathChanged
142- val serverRunning = plugin.isRunning
143- val owned = plugin.ownsProcess
144- val mustConfirmStop = serverRunning && owned && portOrPathChanged
145- val mustReattach = serverRunning && ! owned && portChanged
150+ if (! settingsChanged && ! shouldResolveInfo) return
146151
147- var resolvedInfo : OpenCodeInfo ? = oldOpenCodeInfo
148- var openCodeInfoResolved = false
152+ val mustConfirmStop = plugin.isRunning && plugin.ownsProcess && (portChanged || pathChanged)
153+ val mustReattach = plugin.isRunning && ! plugin.ownsProcess && portChanged
149154
155+ var resolvedInfo = oldOpenCodeInfo
150156 if (shouldResolveInfo) {
151157 val userProvidedPath = newPath.takeIf { it.isNotBlank() }
152- val resolvedRef = AtomicReference <OpenCodeInfo ?>()
153- ProgressManager .getInstance().runProcessWithProgressSynchronously(
154- {
155- resolvedRef.set(executableResolver(userProvidedPath))
156- },
157- " Resolving OpenCode\u2026 " ,
158- false ,
159- project
160- )
161-
162- resolvedInfo = resolvedRef.get()
163- if (isExecutableResolutionFailureBlocking(userProvidedPath, resolvedInfo)) {
164- restore(settings, oldSettings)
158+ resolvedInfo = resolveExecutableInfo(userProvidedPath)
159+ if (userProvidedPath != null && resolvedInfo == null ) {
165160 throw ConfigurationException (
166- " Could not find a valid OpenCode executable. Check the path and try again."
161+ " Could not find a valid OpenCode executable. Check the path and try again." ,
167162 )
168163 }
169-
170- openCodeInfoResolved = true
171164 }
172165
173- if (mustConfirmStop) {
174- val confirmed = Messages .showYesNoDialog(
175- project,
176- " The OpenCode server is currently running. Applying these changes will stop it. " +
177- " You will need to start it again manually." ,
178- " Stop OpenCode Server?" ,
179- " Stop Server" ,
180- " Cancel" ,
181- Messages .getWarningIcon()
182- ) == Messages .YES
183-
184- if (! confirmed) {
185- restore(settings, oldSettings)
186- reset()
187- return
188- }
166+ if (mustConfirmStop && ! confirmStopServerRestart()) {
167+ reset()
168+ return
169+ }
189170
190- plugin.stopServer( )
171+ persistPendingToSettings(settings )
191172
192- } else if (mustReattach) {
193- plugin.reattach(newPort)
173+ when {
174+ mustConfirmStop -> plugin.stopServer()
175+ mustReattach -> plugin.reattach(newPort)
194176 }
195177
196- if (openCodeInfoResolved && resolvedInfo != oldOpenCodeInfo) {
178+ if (shouldResolveInfo && resolvedInfo != oldOpenCodeInfo) {
197179 plugin.openCodeInfo = resolvedInfo
198180 project.messageBus.syncPublisher(OpenCodeInfoChangedListener .TOPIC )
199181 .onOpenCodeInfoChanged(resolvedInfo)
200182 }
201183
202- val finalSettings = snapshot(settings)
203- if (finalSettings != oldSettings) {
184+ if (settingsChanged) {
204185 project.messageBus.syncPublisher(OpenCodeSettingsChangedListener .TOPIC )
205- .onSettingsChanged(oldSettings, finalSettings )
186+ .onSettingsChanged(oldSettings, newSettings )
206187 }
207188
208189 EditorDiffRenderer .getInstance(project).onSettingsChanged()
209190 }
210191
211- private fun restore (settings : OpenCodeSettings , snapshot : OpenCodeSettingsSnapshot ) {
212- settings.serverPort = snapshot.serverPort
213- settings.executablePath = snapshot.executablePath
214- settings.inlineDiffEnabled = snapshot.inlineDiffEnabled
215- settings.diffTraceEnabled = snapshot.diffTraceEnabled
216- settings.diffTraceHistoryEnabled = snapshot.diffTraceHistoryEnabled
217- settings.inlineTerminalEnabled = snapshot.inlineTerminalEnabled
218- settings.terminalEngine = snapshot.terminalEngine
192+ private fun resolveExecutableInfo (userProvidedPath : String? ): OpenCodeInfo ? {
193+ val resolvedRef = AtomicReference <OpenCodeInfo ?>()
194+ ProgressManager .getInstance().runProcessWithProgressSynchronously(
195+ {
196+ resolvedRef.set(executableResolver(userProvidedPath))
197+ },
198+ " Resolving OpenCode..." ,
199+ false ,
200+ project,
201+ )
202+ return resolvedRef.get()
203+ }
204+
205+ private fun confirmStopServerRestart (): Boolean =
206+ Messages .showYesNoDialog(
207+ project,
208+ " The OpenCode server is currently running. Applying these changes will stop it. " +
209+ " You will need to start it again manually." ,
210+ " Stop OpenCode Server?" ,
211+ " Stop Server" ,
212+ " Cancel" ,
213+ Messages .getWarningIcon(),
214+ ) == Messages .YES
215+
216+ private fun loadPendingFromPersisted (settings : OpenCodeSettings = OpenCodeSettings .getInstance(project)) {
217+ pendingState.serverPort = settings.serverPort
218+ pendingState.executablePath = settings.executablePath
219+ pendingState.inlineDiffEnabled = settings.inlineDiffEnabled
220+ pendingState.diffTraceEnabled = settings.diffTraceEnabled
221+ pendingState.diffTraceHistoryEnabled = settings.diffTraceHistoryEnabled
222+ pendingState.inlineTerminalEnabled = settings.inlineTerminalEnabled
223+ pendingState.terminalEngine = settings.terminalEngine
219224 }
220225
221- private fun snapshot (settings : OpenCodeSettings ): OpenCodeSettingsSnapshot = OpenCodeSettingsSnapshot (
222- serverPort = settings.serverPort,
223- executablePath = settings.executablePath,
224- inlineDiffEnabled = settings.inlineDiffEnabled,
225- diffTraceEnabled = settings.diffTraceEnabled,
226- diffTraceHistoryEnabled = settings.diffTraceHistoryEnabled,
227- inlineTerminalEnabled = settings.inlineTerminalEnabled,
228- terminalEngine = settings.terminalEngine,
226+ private fun persistPendingToSettings (settings : OpenCodeSettings ) {
227+ settings.loadState(pendingState.copy())
228+ }
229+
230+ private fun snapshot (state : OpenCodeSettings .State ): OpenCodeSettingsSnapshot = OpenCodeSettingsSnapshot (
231+ serverPort = state.serverPort,
232+ executablePath = state.executablePath,
233+ inlineDiffEnabled = state.inlineDiffEnabled,
234+ diffTraceEnabled = state.diffTraceEnabled,
235+ diffTraceHistoryEnabled = state.diffTraceHistoryEnabled,
236+ inlineTerminalEnabled = state.inlineTerminalEnabled,
237+ terminalEngine = state.terminalEngine,
229238 )
230239}
231-
232- internal fun isExecutableResolutionFailureBlocking (userProvidedPath : String? , resolvedInfo : OpenCodeInfo ? ): Boolean =
233- userProvidedPath != null && resolvedInfo == null
0 commit comments