Skip to content

fix: update&start outdated workspaces #128

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 5 commits into from
Jun 12, 2025
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
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
### Fixed

- `Stop` action is now available for running workspaces that have an out of date template.
- outdated and stopped workspaces are now updated and started when handling URI

## 0.3.0 - 2025-06-10

Expand Down
22 changes: 16 additions & 6 deletions src/main/kotlin/com/coder/toolbox/CoderRemoteProvider.kt
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@ import com.coder.toolbox.util.DialogUi
import com.coder.toolbox.util.withPath
import com.coder.toolbox.views.Action
import com.coder.toolbox.views.AuthWizardPage
import com.coder.toolbox.views.CoderPage
import com.coder.toolbox.views.CoderSettingsPage
import com.coder.toolbox.views.NewEnvironmentPage
import com.coder.toolbox.views.state.AuthWizardState
Expand Down Expand Up @@ -110,7 +109,6 @@ class CoderRemoteProvider(
return@launch
}


// Reconfigure if environments changed.
if (lastEnvironments.size != resolvedEnvironments.size || lastEnvironments != resolvedEnvironments) {
context.logger.info("Workspaces have changed, reconfiguring CLI: $resolvedEnvironments")
Expand Down Expand Up @@ -269,12 +267,25 @@ class CoderRemoteProvider(
* Handle incoming links (like from the dashboard).
*/
override suspend fun handleUri(uri: URI) {
linkHandler.handle(uri, shouldDoAutoLogin()) { restClient, cli ->
linkHandler.handle(
uri, shouldDoAutoLogin(),
{
coderHeaderPage.isBusyCreatingNewEnvironment.update {
true
}
},
{
coderHeaderPage.isBusyCreatingNewEnvironment.update {
false
}
}
) { restClient, cli ->
// stop polling and de-initialize resources
close()
// start initialization with the new settings
this@CoderRemoteProvider.client = restClient
coderHeaderPage = NewEnvironmentPage(context, context.i18n.pnotr(restClient.url.toString()))

environments.showLoadingMessage()
pollJob = poll(restClient, cli)
}
Expand Down Expand Up @@ -332,7 +343,7 @@ class CoderRemoteProvider(

private fun shouldDoAutoLogin(): Boolean = firstRun && context.secrets.rememberMe == true

private fun onConnect(client: CoderRestClient, cli: CoderCLIManager) {
private suspend fun onConnect(client: CoderRestClient, cli: CoderCLIManager) {
// Store the URL and token for use next time.
context.secrets.lastDeploymentURL = client.url.toString()
context.secrets.lastToken = client.token ?: ""
Expand All @@ -344,8 +355,7 @@ class CoderRemoteProvider(
environments.showLoadingMessage()
coderHeaderPage = NewEnvironmentPage(context, context.i18n.pnotr(client.url.toString()))
pollJob = poll(client, cli)
context.ui.showUiPage(CoderPage.emptyPage(context))
goToEnvironmentsPage()
context.refreshMainPage()
}

private fun MutableStateFlow<LoadableState<List<RemoteProviderEnvironment>>>.showLoadingMessage() {
Expand Down
25 changes: 25 additions & 0 deletions src/main/kotlin/com/coder/toolbox/CoderToolboxContext.kt
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package com.coder.toolbox
import com.coder.toolbox.store.CoderSecretsStore
import com.coder.toolbox.store.CoderSettingsStore
import com.coder.toolbox.util.toURL
import com.coder.toolbox.views.CoderPage
import com.jetbrains.toolbox.api.core.diagnostics.Logger
import com.jetbrains.toolbox.api.core.os.LocalDesktopManager
import com.jetbrains.toolbox.api.localization.LocalizableStringFactory
Expand All @@ -13,8 +14,10 @@ import com.jetbrains.toolbox.api.remoteDev.states.EnvironmentStateColorPalette
import com.jetbrains.toolbox.api.remoteDev.ui.EnvironmentUiPageManager
import com.jetbrains.toolbox.api.ui.ToolboxUi
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.delay
import java.net.URL
import java.util.UUID
import kotlin.time.Duration.Companion.milliseconds

@Suppress("UnstableApiUsage")
data class CoderToolboxContext(
Expand Down Expand Up @@ -88,4 +91,26 @@ data class CoderToolboxContext(
i18n.ptrl("OK")
)
}

/**
* Forces the title bar on the main page to be refreshed
*/
suspend fun refreshMainPage() {
// the url/title on the main page is only refreshed if
// we're navigating to the main env page from another page.
// If TBX is already on the main page the title is not refreshed
// hence we force a navigation from a blank page.
ui.showUiPage(CoderPage.emptyPage(this))


// Toolbox uses an internal shared flow with a buffer of 4 items and a DROP_OLDEST strategy.
// Both showUiPage and showPluginEnvironmentsPage send events to this flow.
// If we emit two events back-to-back, the first one often gets dropped and only the second is shown.
// To reduce this risk, we add a small delay to let the UI coroutine process the first event.
// Simply yielding the coroutine isn't reliable, especially right after Toolbox starts via URI handling.
// Based on my testing, a 5–10 ms delay is enough to ensure the blank page is processed,
// while still short enough to be invisible to users.
delay(10.milliseconds)
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could you add a comment for why we need to do a delay here for the uneducated? 😄

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thank you for the feedback, I've addressed it in the code, but here is a summary:

From testing and decompiling the Toolbox code, it seems that TBX uses an internal shared flow with a buffer of 4 items and a DROP_OLDEST strategy. Both showUiPage and showPluginEnvironmentsPage send events to this flow. If we emit two events back-to-back, the first one often gets dropped and only the second is shown. To reduce this risk, we add a small delay to let the UI coroutine process the first event. Simply yielding the coroutine isn't reliable, especially right after Toolbox starts via URI handling. Based on my testing, a 5–10 ms delay is enough to ensure the blank page is processed, while still short enough to be invisible to users.
It goes without saying that the API is limiting and we don't have the callbacks or the hooks necessary to observer when a page goes active. That would be much cleaner.

In fact the whole thing is necessary because the url/title on the main page is only refreshed if we're navigating to the main env page from another page. If TBX is already on the main page the title is not refreshed hence we force a navigation from a blank page. We've raised a ticket in the past, once that is fixed the blank page workaround will no longer be needed and as a consequence the delay will be dropped as well.

envPageManager.showPluginEnvironmentsPage()
}
}
32 changes: 25 additions & 7 deletions src/main/kotlin/com/coder/toolbox/util/CoderProtocolHandler.kt
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,8 @@ open class CoderProtocolHandler(
suspend fun handle(
uri: URI,
shouldWaitForAutoLogin: Boolean,
markAsBusy: () -> Unit,
unmarkAsBusy: () -> Unit,
reInitialize: suspend (CoderRestClient, CoderCLIManager) -> Unit
) {
val params = uri.toQueryParameters()
Expand All @@ -62,16 +64,27 @@ open class CoderProtocolHandler(
val workspaceName = resolveWorkspaceName(params) ?: return
val restClient = buildRestClient(deploymentURL, token) ?: return
val workspace = restClient.workspaces().matchName(workspaceName, deploymentURL) ?: return
if (!prepareWorkspace(workspace, restClient, workspaceName, deploymentURL)) return

// we resolve the agent after the workspace is started otherwise we can get misleading
// errors like: no agent available while workspace is starting or stopping
val agent = resolveAgent(params, workspace) ?: return
if (!ensureAgentIsReady(workspace, agent)) return

val cli = configureCli(deploymentURL, restClient)
reInitialize(restClient, cli)

var agent: WorkspaceAgent
try {
markAsBusy()
context.refreshMainPage()
if (!prepareWorkspace(workspace, restClient, workspaceName, deploymentURL)) return
// we resolve the agent after the workspace is started otherwise we can get misleading
// errors like: no agent available while workspace is starting or stopping
// we also need to retrieve the workspace again to have the latest resources (ex: agent)
// attached to the workspace.
agent = resolveAgent(
params,
restClient.workspace(workspace.id)
) ?: return
if (!ensureAgentIsReady(workspace, agent)) return
} finally {
unmarkAsBusy()
}
val environmentId = "${workspace.name}.${agent.name}"
context.showEnvironmentPage(environmentId)

Expand Down Expand Up @@ -173,7 +186,11 @@ open class CoderProtocolHandler(
}

try {
restClient.startWorkspace(workspace)
if (workspace.outdated) {
restClient.updateWorkspace(workspace)
} else {
restClient.startWorkspace(workspace)
}
} catch (e: Exception) {
context.logAndShowError(
CAN_T_HANDLE_URI_TITLE,
Expand Down Expand Up @@ -428,6 +445,7 @@ open class CoderProtocolHandler(
}
}


private fun CoderToolboxContext.popupPluginMainPage() {
this.ui.showWindow()
this.envPageManager.showPluginEnvironmentsPage(true)
Expand Down
2 changes: 1 addition & 1 deletion src/main/kotlin/com/coder/toolbox/views/AuthWizardPage.kt
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ class AuthWizardPage(
private val context: CoderToolboxContext,
private val settingsPage: CoderSettingsPage,
initialAutoLogin: Boolean = false,
onConnect: (
onConnect: suspend (
client: CoderRestClient,
cli: CoderCLIManager,
) -> Unit,
Expand Down
3 changes: 3 additions & 0 deletions src/main/kotlin/com/coder/toolbox/views/CoderPage.kt
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import com.jetbrains.toolbox.api.core.ui.icons.SvgIcon.IconType
import com.jetbrains.toolbox.api.localization.LocalizableString
import com.jetbrains.toolbox.api.ui.actions.RunnableActionDescription
import com.jetbrains.toolbox.api.ui.components.UiPage
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.launch
import java.util.UUID

Expand Down Expand Up @@ -39,6 +40,8 @@ abstract class CoderPage(
SvgIcon(byteArrayOf(), type = IconType.Masked)
}

override val isBusyCreatingNewEnvironment: MutableStateFlow<Boolean> = MutableStateFlow(false)

/**
* Show an error as a popup on this page.
*/
Expand Down
2 changes: 1 addition & 1 deletion src/main/kotlin/com/coder/toolbox/views/ConnectStep.kt
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ class ConnectStep(
private val shouldAutoLogin: StateFlow<Boolean>,
private val notify: (String, Throwable) -> Unit,
private val refreshWizard: () -> Unit,
private val onConnect: (
private val onConnect: suspend (
client: CoderRestClient,
cli: CoderCLIManager,
) -> Unit,
Expand Down
Loading