@@ -28,8 +28,7 @@ import com.coder.toolbox.views.CoderSettingsPage
2828import com.coder.toolbox.views.NewEnvironmentPage
2929import com.coder.toolbox.views.SuspendBiConsumer
3030import com.coder.toolbox.views.state.CoderOAuthSessionContext
31- import com.coder.toolbox.views.state.CoderSetupWizardContext
32- import com.coder.toolbox.views.state.CoderSetupWizardState
31+ import com.coder.toolbox.views.state.PageRouter
3332import com.coder.toolbox.views.state.WizardStep
3433import com.jetbrains.toolbox.api.core.ui.icons.SvgIcon
3534import com.jetbrains.toolbox.api.core.ui.icons.SvgIcon.IconType
@@ -113,6 +112,8 @@ class CoderRemoteProvider(
113112
114113 private val errorBuffer = mutableListOf<Throwable >()
115114
115+ private val router = PageRouter ()
116+
116117 /* *
117118 * With the provided client, start polling for workspaces. Every time a new
118119 * workspace is added, reconfigure SSH using the provided cli (including the
@@ -269,6 +270,7 @@ class CoderRemoteProvider(
269270 lastEnvironments.clear()
270271 environments.value = LoadableState .Value (emptyList())
271272 isInitialized.update { false }
273+ router.clear()
272274 context.logger.info(" Coder plugin is now closed" )
273275 }
274276
@@ -345,9 +347,6 @@ class CoderRemoteProvider(
345347 */
346348 override suspend fun handleUri (uri : URI ) {
347349 try {
348- // Obtain focus. This switches to the main plugin screen, even
349- // if last opened provider was not Coder
350- context.envPageManager.showPluginEnvironmentsPage()
351350 if (uri.toString().startsWith(" jetbrains://gateway/com.coder.toolbox/auth" )) {
352351 handleOAuthUri(uri)
353352 return
@@ -372,25 +371,25 @@ class CoderRemoteProvider(
372371 linkHandler.handle(params, newUrl, this .client!! , this .cli!! )
373372 coderHeaderPage.isBusy.update { false }
374373 } else {
375- // Different URL - we need a new connection.
376- // Chain the link handling after onConnect so it runs once the connection is established.
377- CoderSetupWizardContext .apply {
378- url = newUrl
379- token = newToken
380- }
381- CoderSetupWizardState .goToStep(WizardStep .CONNECT )
382- context.ui.showUiPage(
383- CoderCliSetupWizardPage (
384- context, settingsPage, visibilityState,
385- initialAutoSetup = true ,
386- jumpToMainPageOnError = true ,
387- onConnect = onConnect.andThen(deferredLinkHandler(params, newUrl))
388- .andThen { _, _ ->
389- coderHeaderPage.isBusy.update { false }
390- },
391- onTokenRefreshed = ::onTokenRefreshed
392- )
374+ // Different URL - we need a new connection. Tear down any
375+ // in-flight wizard, install a fresh one on the router, and let
376+ // showPluginEnvironmentsPage() pull it through getOverrideUiPage.
377+ router.activeWizard?.dispose()
378+ val wizard = CoderCliSetupWizardPage (
379+ context, settingsPage, visibilityState,
380+ initialAutoSetup = true ,
381+ jumpToMainPageOnError = true ,
382+ onConnect = onConnect.andThen(deferredLinkHandler(params, newUrl))
383+ .andThen { _, _ ->
384+ coderHeaderPage.isBusy.update { false }
385+ },
386+ onTokenRefreshed = ::onTokenRefreshed
393387 )
388+ wizard.model.url = newUrl
389+ wizard.model.token = newToken
390+ wizard.model.goTo(WizardStep .CONNECT )
391+ router.replaceWith(wizard)
392+ context.envPageManager.showPluginEnvironmentsPage()
394393 }
395394 } catch (ex: Exception ) {
396395 val textError = if (ex is APIResponseException ) {
@@ -403,7 +402,6 @@ class CoderRemoteProvider(
403402 textError ? : " "
404403 )
405404 coderHeaderPage.isBusy.update { false }
406- context.envPageManager.showPluginEnvironmentsPage()
407405 } finally {
408406 firstRun = false
409407 }
@@ -424,7 +422,15 @@ class CoderRemoteProvider(
424422 )
425423 }
426424
427- params[" state" ]?.takeIf { it == CoderSetupWizardContext .oauthSession?.state }
425+ val activeWizard = router.activeWizard ? : return context.logAndShowError(
426+ FAILED_TO_HANDLE_OAUTH2_TITLE ,
427+ " OAuth2 callback arrived but the setup wizard is no longer active"
428+ )
429+ val activeOAuthSession = activeWizard.model.oauthSession ? : return context.logAndShowError(
430+ FAILED_TO_HANDLE_OAUTH2_TITLE ,
431+ " OAuth2 callback arrived but no OAuth session was started"
432+ )
433+ params[" state" ]?.takeIf { it == activeOAuthSession.state }
428434 ? : return context.logAndShowError(
429435 FAILED_TO_HANDLE_OAUTH2_TITLE ,
430436 " Server responded back with an invalid state that does not match the initial authorization state sent to the server"
@@ -442,18 +448,20 @@ class CoderRemoteProvider(
442448 )
443449 return
444450 }
445- exchangeOAuthCodeForToken(code, CoderSetupWizardContext .oauthSession !! )
451+ exchangeOAuthCodeForToken(code, activeOAuthSession, activeWizard )
446452 }
447453
448- private suspend fun exchangeOAuthCodeForToken (code : String , oauthSessionContext : CoderOAuthSessionContext ) {
454+ private suspend fun exchangeOAuthCodeForToken (
455+ code : String ,
456+ oauthSessionContext : CoderOAuthSessionContext ,
457+ wizard : CoderCliSetupWizardPage ,
458+ ) {
449459 try {
450460 context.logger.info(" Handling OAuth callback..." )
451461
452462 val tokenResponse = OAuth2Client (context).exchangeCode(oauthSessionContext, code)
453- CoderSetupWizardContext .oauthSession = oauthSessionContext.copy(tokenResponse = tokenResponse)
454-
455- CoderSetupWizardState .goToStep(WizardStep .CONNECT )
456-
463+ wizard.model.oauthSession = oauthSessionContext.copy(tokenResponse = tokenResponse)
464+ wizard.model.goTo(WizardStep .CONNECT )
457465 } catch (e: Exception ) {
458466 context.logAndShowError(" OAuth Error" , " Exception during token exchange: ${e.message} " , e)
459467 }
@@ -520,67 +528,70 @@ class CoderRemoteProvider(
520528 * list.
521529 */
522530 override fun getOverrideUiPage (): UiPage ? {
523- // Show the setup page if we have not configured the client yet.
524- if (client == null ) {
525- // When coming back to the application, initializeSession immediately.
526- if (shouldDoAutoSetup()) {
527- try {
528- val storedOAuthSession = context.secrets.oauthSessionFor(context.deploymentUrl.toString())
529- CoderSetupWizardContext .apply {
530- url = context.deploymentUrl
531- token = context.secrets.apiTokenFor(context.deploymentUrl)
532- if (storedOAuthSession != null ) {
533- oauthSession = CoderOAuthSessionContext (
534- clientId = storedOAuthSession.clientId,
535- clientSecret = storedOAuthSession.clientSecret,
536- tokenCodeVerifier = " " ,
537- state = " " ,
538- tokenEndpoint = storedOAuthSession.tokenEndpoint,
539- tokenAuthMethod = storedOAuthSession.tokenAuthMethod,
540- tokenResponse = OAuthTokenResponse (
541- accessToken = " " ,
542- tokenType = " " ,
543- expiresIn = null ,
544- refreshToken = storedOAuthSession.refreshToken,
545- scope = null
546- )
547- )
548- }
549- }
550- CoderSetupWizardState .goToStep(WizardStep .CONNECT )
551- return CoderCliSetupWizardPage (
552- context, settingsPage, visibilityState,
553- initialAutoSetup = true ,
554- jumpToMainPageOnError = false ,
555- onConnect = onConnect,
556- onTokenRefreshed = ::onTokenRefreshed
557- )
558- } catch (ex: Exception ) {
559- errorBuffer.add(ex)
560- } finally {
561- firstRun = false
562- }
563- }
531+ if (client != null ) return null
532+ return router.requireWizard { buildSetupWizard() }
533+ }
564534
565- // Login flow.
566- CoderSetupWizardState .goToFirstStep()
567- val setupWizardPage =
568- CoderCliSetupWizardPage (
569- context,
570- settingsPage,
571- visibilityState,
535+ /* *
536+ * Build the wizard for the current state. Called once per provider lifetime
537+ * (until [close] clears the router); subsequent visibility cycles reuse the
538+ * same instance, preserving any in-flight connect job.
539+ */
540+ private fun buildSetupWizard (): CoderCliSetupWizardPage {
541+ // When coming back to the application, initializeSession immediately.
542+ if (shouldDoAutoSetup()) {
543+ try {
544+ val storedOAuthSession = context.secrets.oauthSessionFor(context.deploymentUrl.toString())
545+ val wizard = CoderCliSetupWizardPage (
546+ context, settingsPage, visibilityState,
547+ initialAutoSetup = true ,
548+ jumpToMainPageOnError = false ,
572549 onConnect = onConnect,
573550 onTokenRefreshed = ::onTokenRefreshed
574551 )
575- // We might have navigated here due to a polling error.
576- errorBuffer.forEach {
577- setupWizardPage.notify(" Error encountered" , it)
552+ wizard.model.url = context.deploymentUrl
553+ wizard.model.token = context.secrets.apiTokenFor(context.deploymentUrl)
554+ if (storedOAuthSession != null ) {
555+ wizard.model.oauthSession = CoderOAuthSessionContext (
556+ clientId = storedOAuthSession.clientId,
557+ clientSecret = storedOAuthSession.clientSecret,
558+ tokenCodeVerifier = " " ,
559+ state = " " ,
560+ tokenEndpoint = storedOAuthSession.tokenEndpoint,
561+ tokenAuthMethod = storedOAuthSession.tokenAuthMethod,
562+ tokenResponse = OAuthTokenResponse (
563+ accessToken = " " ,
564+ tokenType = " " ,
565+ expiresIn = null ,
566+ refreshToken = storedOAuthSession.refreshToken,
567+ scope = null
568+ )
569+ )
570+ }
571+ wizard.model.goTo(WizardStep .CONNECT )
572+ return wizard
573+ } catch (ex: Exception ) {
574+ errorBuffer.add(ex)
575+ } finally {
576+ firstRun = false
578577 }
579- errorBuffer.clear()
580- // and now reset the errors, otherwise we show it every time on the screen
581- return setupWizardPage
582578 }
583- return null
579+
580+ // Login flow.
581+ val setupWizardPage = CoderCliSetupWizardPage (
582+ context,
583+ settingsPage,
584+ visibilityState,
585+ onConnect = onConnect,
586+ onTokenRefreshed = ::onTokenRefreshed
587+ )
588+ setupWizardPage.model.goToFirst()
589+ // We might have navigated here due to a polling error.
590+ errorBuffer.forEach {
591+ setupWizardPage.notify(" Error encountered" , it)
592+ }
593+ errorBuffer.clear()
594+ return setupWizardPage
584595 }
585596
586597 /* *
0 commit comments