Skip to content

Commit e1e525b

Browse files
committed
fix: show errors when TBX is visible after being minimized
Errors encountered while TBX was running but the window was not visible were never displayed by TBX. This fix queues the errors while TBX is minimized, and they will be displayed again only when visible. This implementation is possible due to an observable state object that can provide information about TBX and plugin visibility. Among other things we also display a more human friendly version for the exceptions raised by the http client during (but not only) workspace polling.
1 parent 99e7835 commit e1e525b

File tree

6 files changed

+65
-21
lines changed

6 files changed

+65
-21
lines changed

CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,10 @@
22

33
## Unreleased
44

5+
### Fixed
6+
7+
- show errors when the Toolbox is visible again after being minimized.
8+
59
## 0.2.3 - 2025-05-26
610

711
### Changed

src/main/kotlin/com/coder/toolbox/CoderRemoteProvider.kt

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,13 @@ class CoderRemoteProvider(
7070
LoadableState.Loading
7171
)
7272

73+
private val visibilityState = MutableStateFlow(
74+
ProviderVisibilityState(
75+
applicationVisible = false,
76+
providerVisible = false
77+
)
78+
)
79+
7380
/**
7481
* With the provided client, start polling for workspaces. Every time a new
7582
* workspace is added, reconfigure SSH using the provided cli (including the
@@ -263,7 +270,11 @@ class CoderRemoteProvider(
263270
* a place to put a timer ("last updated 10 seconds ago" for example)
264271
* and a manual refresh button.
265272
*/
266-
override fun setVisible(visibilityState: ProviderVisibilityState) {}
273+
override fun setVisible(visibility: ProviderVisibilityState) {
274+
visibilityState.update {
275+
visibility
276+
}
277+
}
267278

268279
/**
269280
* Handle incoming links (like from the dashboard).
@@ -309,7 +320,7 @@ class CoderRemoteProvider(
309320
if (autologin && lastDeploymentURL.isNotBlank() && (lastToken.isNotBlank() || !settings.requireTokenAuth)) {
310321
try {
311322
AuthWizardState.goToStep(WizardStep.LOGIN)
312-
return AuthWizardPage(context, settingsPage, true, ::onConnect)
323+
return AuthWizardPage(context, settingsPage, visibilityState, true, ::onConnect)
313324
} catch (ex: Exception) {
314325
errorBuffer.add(ex)
315326
}
@@ -319,7 +330,7 @@ class CoderRemoteProvider(
319330
firstRun = false
320331

321332
// Login flow.
322-
val authWizard = AuthWizardPage(context, settingsPage, false, ::onConnect)
333+
val authWizard = AuthWizardPage(context, settingsPage, visibilityState, onConnect = ::onConnect)
323334
// We might have navigated here due to a polling error.
324335
errorBuffer.forEach {
325336
authWizard.notify("Error encountered", it)

src/main/kotlin/com/coder/toolbox/sdk/ex/APIResponseException.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ import java.net.URL
88
class APIResponseException(action: String, url: URL, code: Int, errorResponse: ApiErrorResponse?) :
99
IOException(formatToPretty(action, url, code, errorResponse)) {
1010

11-
11+
val reason = errorResponse?.detail
1212
val isUnauthorized = HttpURLConnection.HTTP_UNAUTHORIZED == code
1313

1414
companion object {

src/main/kotlin/com/coder/toolbox/views/AuthWizardPage.kt

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,18 +3,23 @@ package com.coder.toolbox.views
33
import com.coder.toolbox.CoderToolboxContext
44
import com.coder.toolbox.cli.CoderCLIManager
55
import com.coder.toolbox.sdk.CoderRestClient
6+
import com.coder.toolbox.sdk.ex.APIResponseException
67
import com.coder.toolbox.util.toURL
78
import com.coder.toolbox.views.state.AuthContext
89
import com.coder.toolbox.views.state.AuthWizardState
910
import com.coder.toolbox.views.state.WizardStep
11+
import com.jetbrains.toolbox.api.remoteDev.ProviderVisibilityState
1012
import com.jetbrains.toolbox.api.ui.actions.RunnableActionDescription
1113
import com.jetbrains.toolbox.api.ui.components.UiField
1214
import kotlinx.coroutines.flow.MutableStateFlow
1315
import kotlinx.coroutines.flow.update
16+
import kotlinx.coroutines.launch
17+
import java.util.UUID
1418

1519
class AuthWizardPage(
1620
private val context: CoderToolboxContext,
1721
private val settingsPage: CoderSettingsPage,
22+
private val visibilityState: MutableStateFlow<ProviderVisibilityState>,
1823
initialAutoLogin: Boolean = false,
1924
onConnect: (
2025
client: CoderRestClient,
@@ -42,6 +47,8 @@ class AuthWizardPage(
4247
override val fields: MutableStateFlow<List<UiField>> = MutableStateFlow(emptyList())
4348
override val actionButtons: MutableStateFlow<List<RunnableActionDescription>> = MutableStateFlow(emptyList())
4449

50+
private val errorBuffer = mutableListOf<Throwable>()
51+
4552
init {
4653
if (shouldAutoLogin.value) {
4754
AuthContext.url = context.secrets.lastDeploymentURL.toURL()
@@ -51,6 +58,12 @@ class AuthWizardPage(
5158

5259
override fun beforeShow() {
5360
displaySteps()
61+
if (errorBuffer.isNotEmpty() && visibilityState.value.applicationVisible) {
62+
errorBuffer.forEach {
63+
showError(it)
64+
}
65+
errorBuffer.clear()
66+
}
5467
}
5568

5669
private fun displaySteps() {
@@ -113,4 +126,34 @@ class AuthWizardPage(
113126
}
114127
}
115128
}
129+
130+
/**
131+
* Show an error as a popup on this page.
132+
*/
133+
fun notify(logPrefix: String, ex: Throwable) {
134+
context.logger.error(ex, logPrefix)
135+
if (!visibilityState.value.applicationVisible) {
136+
context.logger.debug("Toolbox is not yet visible, scheduling error to be displayed later")
137+
errorBuffer.add(ex)
138+
return
139+
}
140+
showError(ex)
141+
}
142+
143+
private fun showError(ex: Throwable) {
144+
val textError = if (ex is APIResponseException) {
145+
if (!ex.reason.isNullOrBlank()) {
146+
ex.reason
147+
} else ex.message
148+
} else ex.message
149+
150+
context.cs.launch {
151+
context.ui.showSnackbar(
152+
UUID.randomUUID().toString(),
153+
context.i18n.ptrl("Error encountered during authentication"),
154+
context.i18n.pnotr(textError ?: ""),
155+
context.i18n.ptrl("Dismiss")
156+
)
157+
}
158+
}
116159
}

src/main/kotlin/com/coder/toolbox/views/CoderPage.kt

Lines changed: 0 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,6 @@ import com.jetbrains.toolbox.api.core.ui.icons.SvgIcon.IconType
66
import com.jetbrains.toolbox.api.localization.LocalizableString
77
import com.jetbrains.toolbox.api.ui.actions.RunnableActionDescription
88
import com.jetbrains.toolbox.api.ui.components.UiPage
9-
import kotlinx.coroutines.launch
10-
import java.util.UUID
119

1210
/**
1311
* Base page that handles the icon, displaying error notifications, and
@@ -39,21 +37,6 @@ abstract class CoderPage(
3937
SvgIcon(byteArrayOf(), type = IconType.Masked)
4038
}
4139

42-
/**
43-
* Show an error as a popup on this page.
44-
*/
45-
fun notify(logPrefix: String, ex: Throwable) {
46-
context.logger.error(ex, logPrefix)
47-
context.cs.launch {
48-
context.ui.showSnackbar(
49-
UUID.randomUUID().toString(),
50-
context.i18n.pnotr(logPrefix),
51-
context.i18n.pnotr(ex.message ?: ""),
52-
context.i18n.ptrl("Dismiss")
53-
)
54-
}
55-
}
56-
5740
companion object {
5841
fun emptyPage(ctx: CoderToolboxContext): UiPage = UiPage(ctx.i18n.pnotr(""))
5942
}

src/main/resources/localization/defaultMessages.po

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -137,4 +137,7 @@ msgid "Network Status"
137137
msgstr ""
138138

139139
msgid "Create workspace"
140+
msgstr ""
141+
142+
msgid "Error encountered during authentication"
140143
msgstr ""

0 commit comments

Comments
 (0)