Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,10 @@

- improved diagnose support

### Fixed

- NPE during error reporting

## 0.6.3 - 2025-08-25

### Added
Expand Down
1 change: 0 additions & 1 deletion src/main/kotlin/com/coder/toolbox/CoderRemoteProvider.kt
Original file line number Diff line number Diff line change
Expand Up @@ -409,7 +409,6 @@ class CoderRemoteProvider(
context.logger.info("Displaying ${client.url} in the UI")
pollJob = poll(client, cli)
context.logger.info("Workspace poll job created with reference $pollJob")
context.envPageManager.showPluginEnvironmentsPage()
}

private fun MutableStateFlow<LoadableState<List<CoderRemoteEnvironment>>>.showLoadingMessage() {
Expand Down
47 changes: 7 additions & 40 deletions src/main/kotlin/com/coder/toolbox/views/CoderCliSetupWizardPage.kt
Original file line number Diff line number Diff line change
Expand Up @@ -3,22 +3,19 @@ package com.coder.toolbox.views
import com.coder.toolbox.CoderToolboxContext
import com.coder.toolbox.cli.CoderCLIManager
import com.coder.toolbox.sdk.CoderRestClient
import com.coder.toolbox.sdk.ex.APIResponseException
import com.coder.toolbox.views.state.CoderCliSetupWizardState
import com.coder.toolbox.views.state.WizardStep
import com.jetbrains.toolbox.api.remoteDev.ProviderVisibilityState
import com.jetbrains.toolbox.api.ui.actions.RunnableActionDescription
import com.jetbrains.toolbox.api.ui.components.UiField
import kotlinx.coroutines.CoroutineName
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch
import java.util.UUID

class CoderCliSetupWizardPage(
private val context: CoderToolboxContext,
private val settingsPage: CoderSettingsPage,
private val visibilityState: MutableStateFlow<ProviderVisibilityState>,
visibilityState: StateFlow<ProviderVisibilityState>,
initialAutoSetup: Boolean = false,
jumpToMainPageOnError: Boolean = false,
onConnect: suspend (
Expand All @@ -31,33 +28,28 @@ class CoderCliSetupWizardPage(
context.ui.showUiPage(settingsPage)
})

private val deploymentUrlStep = DeploymentUrlStep(context, this::notify)
private val deploymentUrlStep = DeploymentUrlStep(context, visibilityState)
private val tokenStep = TokenStep(context)
private val connectStep = ConnectStep(
context,
shouldAutoLogin = shouldAutoSetup,
jumpToMainPageOnError,
this::notify,
visibilityState,
this::displaySteps,
onConnect
)
private val errorReporter = ErrorReporter.create(context, visibilityState, this.javaClass)

/**
* Fields for this page, displayed in order.
*/
override val fields: MutableStateFlow<List<UiField>> = MutableStateFlow(emptyList())
override val actionButtons: MutableStateFlow<List<RunnableActionDescription>> = MutableStateFlow(emptyList())

private val errorBuffer = mutableListOf<Throwable>()

override fun beforeShow() {
displaySteps()
if (errorBuffer.isNotEmpty() && visibilityState.value.applicationVisible) {
errorBuffer.forEach {
showError(it)
}
errorBuffer.clear()
}
errorReporter.flush()
}

private fun displaySteps() {
Expand Down Expand Up @@ -124,30 +116,5 @@ class CoderCliSetupWizardPage(
/**
* Show an error as a popup on this page.
*/
fun notify(logPrefix: String, ex: Throwable) {
context.logger.error(ex, logPrefix)
if (!visibilityState.value.applicationVisible) {
context.logger.debug("Toolbox is not yet visible, scheduling error to be displayed later")
errorBuffer.add(ex)
return
}
showError(ex)
}

private fun showError(ex: Throwable) {
val textError = if (ex is APIResponseException) {
if (!ex.reason.isNullOrBlank()) {
ex.reason
} else ex.message
} else ex.message

context.cs.launch(CoroutineName("Coder Setup Visual Error Reporting")) {
context.ui.showSnackbar(
UUID.randomUUID().toString(),
context.i18n.ptrl("Error encountered while setting up Coder"),
context.i18n.pnotr(textError ?: ""),
context.i18n.ptrl("Dismiss")
)
}
}
fun notify(message: String, ex: Throwable) = errorReporter.report(message, ex)
}
25 changes: 15 additions & 10 deletions src/main/kotlin/com/coder/toolbox/views/ConnectStep.kt
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import com.coder.toolbox.plugin.PluginManager
import com.coder.toolbox.sdk.CoderRestClient
import com.coder.toolbox.views.state.CoderCliSetupContext
import com.coder.toolbox.views.state.CoderCliSetupWizardState
import com.jetbrains.toolbox.api.remoteDev.ProviderVisibilityState
import com.jetbrains.toolbox.api.ui.components.LabelField
import com.jetbrains.toolbox.api.ui.components.RowGroup
import com.jetbrains.toolbox.api.ui.components.ValidationErrorField
Expand All @@ -27,24 +28,23 @@ class ConnectStep(
private val context: CoderToolboxContext,
private val shouldAutoLogin: StateFlow<Boolean>,
private val jumpToMainPageOnError: Boolean,
private val notify: (String, Throwable) -> Unit,
visibilityState: StateFlow<ProviderVisibilityState>,
private val refreshWizard: () -> Unit,
private val onConnect: suspend (
client: CoderRestClient,
cli: CoderCLIManager,
) -> Unit,
private val onConnect: suspend (client: CoderRestClient, cli: CoderCLIManager) -> Unit,
) : WizardStep {
private var signInJob: Job? = null

private val statusField = LabelField(context.i18n.pnotr(""))
private val errorField = ValidationErrorField(context.i18n.pnotr(""))
private val errorReporter = ErrorReporter.create(context, visibilityState, this.javaClass)

override val panel: RowGroup = RowGroup(
RowGroup.RowField(statusField),
RowGroup.RowField(errorField)
)

override fun onVisible() {
errorReporter.flush()
errorField.textState.update {
context.i18n.pnotr("")
}
Expand Down Expand Up @@ -73,6 +73,9 @@ class ConnectStep(
errorField.textState.update { context.i18n.ptrl("Token is required") }
return
}
// Capture the host name early for error reporting
val hostName = CoderCliSetupContext.url!!.host

signInJob?.cancel()
signInJob = context.cs.launch(CoroutineName("Http and CLI Setup")) {
try {
Expand Down Expand Up @@ -100,21 +103,23 @@ class ConnectStep(
yield()
cli.login(client.token!!)
}
logAndReportProgress("Successfully configured ${CoderCliSetupContext.url!!.host}...")
logAndReportProgress("Successfully configured ${hostName}...")
// allows interleaving with the back/cancel action
yield()
CoderCliSetupContext.reset()
CoderCliSetupWizardState.goToFirstStep()
context.logger.info("Connection setup done, initializing the workspace poller...")
onConnect(client, cli)

CoderCliSetupContext.reset()
CoderCliSetupWizardState.goToFirstStep()
context.envPageManager.showPluginEnvironmentsPage()
} catch (ex: CancellationException) {
if (ex.message != USER_HIT_THE_BACK_BUTTON) {
notify("Connection to ${CoderCliSetupContext.url!!.host} was configured", ex)
errorReporter.report("Connection to $hostName was configured", ex)
handleNavigation()
refreshWizard()
}
} catch (ex: Exception) {
notify("Failed to configure ${CoderCliSetupContext.url!!.host}", ex)
errorReporter.report("Failed to configure $hostName", ex)
handleNavigation()
refreshWizard()
}
Expand Down
9 changes: 7 additions & 2 deletions src/main/kotlin/com/coder/toolbox/views/DeploymentUrlStep.kt
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,15 @@ import com.coder.toolbox.util.toURL
import com.coder.toolbox.util.validateStrictWebUrl
import com.coder.toolbox.views.state.CoderCliSetupContext
import com.coder.toolbox.views.state.CoderCliSetupWizardState
import com.jetbrains.toolbox.api.remoteDev.ProviderVisibilityState
import com.jetbrains.toolbox.api.ui.components.CheckboxField
import com.jetbrains.toolbox.api.ui.components.LabelField
import com.jetbrains.toolbox.api.ui.components.LabelStyleType
import com.jetbrains.toolbox.api.ui.components.RowGroup
import com.jetbrains.toolbox.api.ui.components.TextField
import com.jetbrains.toolbox.api.ui.components.TextType
import com.jetbrains.toolbox.api.ui.components.ValidationErrorField
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.update
import java.net.MalformedURLException
import java.net.URL
Expand All @@ -25,9 +27,11 @@ import java.net.URL
*/
class DeploymentUrlStep(
private val context: CoderToolboxContext,
private val notify: (String, Throwable) -> Unit
visibilityState: StateFlow<ProviderVisibilityState>,
) :
WizardStep {
private val errorReporter = ErrorReporter.create(context, visibilityState, this.javaClass)

private val urlField = TextField(context.i18n.ptrl("Deployment URL"), "", TextType.General)
private val emptyLine = LabelField(context.i18n.pnotr(""), LabelStyleType.Normal)

Expand Down Expand Up @@ -66,6 +70,7 @@ class DeploymentUrlStep(
signatureFallbackStrategyField.checkedState.update {
context.settingsStore.fallbackOnCoderForSignatures.isAllowed()
}
errorReporter.flush()
}

override fun onNext(): Boolean {
Expand All @@ -78,7 +83,7 @@ class DeploymentUrlStep(
try {
CoderCliSetupContext.url = validateRawUrl(url)
} catch (e: MalformedURLException) {
notify("URL is invalid", e)
errorReporter.report("URL is invalid", e)
return false
}
if (context.settingsStore.requireTokenAuth) {
Expand Down
73 changes: 73 additions & 0 deletions src/main/kotlin/com/coder/toolbox/views/ErrorReporter.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
package com.coder.toolbox.views

import com.coder.toolbox.CoderToolboxContext
import com.coder.toolbox.sdk.ex.APIResponseException
import com.jetbrains.toolbox.api.remoteDev.ProviderVisibilityState
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.launch
import java.util.UUID

sealed class ErrorReporter {

/**
* Logs and show errors as popups.
*/
abstract fun report(message: String, ex: Throwable)

/**
* Processes any buffered errors when the application becomes visible.
*/
abstract fun flush()

companion object {
fun create(
context: CoderToolboxContext,
visibilityState: StateFlow<ProviderVisibilityState>,
callerClass: Class<*>
): ErrorReporter = ErrorReporterImpl(context, visibilityState, callerClass)
}
}

private class ErrorReporterImpl(
private val context: CoderToolboxContext,
private val visibilityState: StateFlow<ProviderVisibilityState>,
private val callerClass: Class<*>
) : ErrorReporter() {
private val errorBuffer = mutableListOf<Throwable>()

override fun report(message: String, ex: Throwable) {
context.logger.error(ex, "[${callerClass.simpleName}] $message")
if (!visibilityState.value.applicationVisible) {
context.logger.debug("Toolbox is not yet visible, scheduling error to be displayed later")
errorBuffer.add(ex)
return
}
showError(ex)
}

private fun showError(ex: Throwable) {
val textError = if (ex is APIResponseException) {
if (!ex.reason.isNullOrBlank()) {
ex.reason
} else ex.message
} else ex.message ?: ex.toString()
context.cs.launch {
context.ui.showSnackbar(
UUID.randomUUID().toString(),
context.i18n.ptrl("Error encountered while setting up Coder"),
context.i18n.pnotr(textError ?: ""),
context.i18n.ptrl("Dismiss")
)
}
}


override fun flush() {
if (errorBuffer.isNotEmpty() && visibilityState.value.applicationVisible) {
errorBuffer.forEach {
showError(it)
}
errorBuffer.clear()
}
}
}
Loading