From a34d1ddc8869225c22b9071c480e3e9e82551c2d Mon Sep 17 00:00:00 2001 From: Faur Ioan-Aurel Date: Mon, 10 Nov 2025 23:42:01 +0200 Subject: [PATCH 1/9] impl: setting to configure the title source on the main page This commit adds a option `useAppNameAsTitle` that allows users to specify whether or not they want to use the application name visible in the dashboard as Tbx main tile instead of the URL. Default is to use the URL. --- .../com/coder/toolbox/settings/ReadOnlyCoderSettings.kt | 6 ++++++ .../kotlin/com/coder/toolbox/store/CoderSettingsStore.kt | 5 +++++ src/main/kotlin/com/coder/toolbox/store/StoreKeys.kt | 2 ++ 3 files changed, 13 insertions(+) diff --git a/src/main/kotlin/com/coder/toolbox/settings/ReadOnlyCoderSettings.kt b/src/main/kotlin/com/coder/toolbox/settings/ReadOnlyCoderSettings.kt index 8eed699..edf4801 100644 --- a/src/main/kotlin/com/coder/toolbox/settings/ReadOnlyCoderSettings.kt +++ b/src/main/kotlin/com/coder/toolbox/settings/ReadOnlyCoderSettings.kt @@ -19,6 +19,12 @@ interface ReadOnlyCoderSettings { */ val defaultURL: String + /** + * Whether to display the application name instead of the URL + * in the main screen. Defaults to URL + */ + val useAppNameAsTitle: Boolean + /** * Used to download the Coder CLI which is necessary to proxy SSH * connections. The If-None-Match header will be set to the SHA1 of the CLI diff --git a/src/main/kotlin/com/coder/toolbox/store/CoderSettingsStore.kt b/src/main/kotlin/com/coder/toolbox/store/CoderSettingsStore.kt index becdea0..e4ae45b 100644 --- a/src/main/kotlin/com/coder/toolbox/store/CoderSettingsStore.kt +++ b/src/main/kotlin/com/coder/toolbox/store/CoderSettingsStore.kt @@ -38,6 +38,7 @@ class CoderSettingsStore( // Properties implementation override val lastDeploymentURL: String? get() = store[LAST_USED_URL] override val defaultURL: String get() = store[DEFAULT_URL] ?: "https://dev.coder.com" + override val useAppNameAsTitle: Boolean = store[APP_NAME_AS_TITLE]?.toBooleanStrictOrNull() ?: false override val binarySource: String? get() = store[BINARY_SOURCE] override val binaryDirectory: String? get() = store[BINARY_DIRECTORY] override val disableSignatureVerification: Boolean @@ -165,6 +166,10 @@ class CoderSettingsStore( store[LAST_USED_URL] = url.toString() } + fun updateUseAppNameAsTitle(appNameAsTitle: Boolean) { + store[APP_NAME_AS_TITLE] = appNameAsTitle.toString() + } + fun updateBinarySource(source: String) { store[BINARY_SOURCE] = source } diff --git a/src/main/kotlin/com/coder/toolbox/store/StoreKeys.kt b/src/main/kotlin/com/coder/toolbox/store/StoreKeys.kt index d38631a..bc46c4f 100644 --- a/src/main/kotlin/com/coder/toolbox/store/StoreKeys.kt +++ b/src/main/kotlin/com/coder/toolbox/store/StoreKeys.kt @@ -6,6 +6,8 @@ internal const val LAST_USED_URL = "lastDeploymentURL" internal const val DEFAULT_URL = "defaultURL" +internal const val APP_NAME_AS_TITLE = "useAppNameAsTitle" + internal const val BINARY_SOURCE = "binarySource" internal const val BINARY_DIRECTORY = "binaryDirectory" From 925dcb92c01228e6b191de512f75fd4234ec4368 Mon Sep 17 00:00:00 2001 From: Faur Ioan-Aurel Date: Mon, 10 Nov 2025 23:44:22 +0200 Subject: [PATCH 2/9] impl: ability to configure `useAppNameAsTitle` from UI settings This is an option that more users would probably like to use. --- .../kotlin/com/coder/toolbox/views/CoderSettingsPage.kt | 7 +++++++ src/main/resources/localization/defaultMessages.po | 3 +++ 2 files changed, 10 insertions(+) diff --git a/src/main/kotlin/com/coder/toolbox/views/CoderSettingsPage.kt b/src/main/kotlin/com/coder/toolbox/views/CoderSettingsPage.kt index 5d5f115..99bb3ad 100644 --- a/src/main/kotlin/com/coder/toolbox/views/CoderSettingsPage.kt +++ b/src/main/kotlin/com/coder/toolbox/views/CoderSettingsPage.kt @@ -41,6 +41,8 @@ class CoderSettingsPage(private val context: CoderToolboxContext, triggerSshConf TextField(context.i18n.ptrl("Data directory"), settings.dataDirectory ?: "", TextType.General) private val enableDownloadsField = CheckboxField(settings.enableDownloads, context.i18n.ptrl("Enable downloads")) + private val useAppNameField = + CheckboxField(settings.useAppNameAsTitle, context.i18n.ptrl("Use app name as main page title instead of URL")) private val disableSignatureVerificationField = CheckboxField( settings.disableSignatureVerification, @@ -95,6 +97,7 @@ class CoderSettingsPage(private val context: CoderToolboxContext, triggerSshConf listOf( binarySourceField, enableDownloadsField, + useAppNameField, binaryDirectoryField, enableBinaryDirectoryFallbackField, disableSignatureVerificationField, @@ -121,6 +124,7 @@ class CoderSettingsPage(private val context: CoderToolboxContext, triggerSshConf context.settingsStore.updateBinaryDirectory(binaryDirectoryField.contentState.value) context.settingsStore.updateDataDirectory(dataDirectoryField.contentState.value) context.settingsStore.updateEnableDownloads(enableDownloadsField.checkedState.value) + context.settingsStore.updateUseAppNameAsTitle(useAppNameField.checkedState.value) context.settingsStore.updateDisableSignatureVerification(disableSignatureVerificationField.checkedState.value) context.settingsStore.updateSignatureFallbackStrategy(signatureFallbackStrategyField.checkedState.value) context.settingsStore.updateHttpClientLogLevel(httpLoggingField.selectedValueState.value) @@ -164,6 +168,9 @@ class CoderSettingsPage(private val context: CoderToolboxContext, triggerSshConf enableDownloadsField.checkedState.update { settings.enableDownloads } + useAppNameField.checkedState.update { + settings.useAppNameAsTitle + } signatureFallbackStrategyField.checkedState.update { settings.fallbackOnCoderForSignatures.isAllowed() } diff --git a/src/main/resources/localization/defaultMessages.po b/src/main/resources/localization/defaultMessages.po index 29351e3..16b6ed5 100644 --- a/src/main/resources/localization/defaultMessages.po +++ b/src/main/resources/localization/defaultMessages.po @@ -189,3 +189,6 @@ msgstr "" msgid "Workspace name" msgstr "" + +msgid "Use app name as main page title instead of URL" +msgstr "" \ No newline at end of file From cdaf544fd47576ad0c83fcd70b26a1ad8421da5d Mon Sep 17 00:00:00 2001 From: Faur Ioan-Aurel Date: Mon, 10 Nov 2025 23:54:51 +0200 Subject: [PATCH 3/9] impl: add http client stubs Adds the http client stubs necessary to download the application name via the REST API. --- .../com/coder/toolbox/sdk/CoderRestClient.kt | 24 ++++++++++++++++++- .../coder/toolbox/sdk/v2/CoderV2RestFacade.kt | 7 ++++++ .../coder/toolbox/sdk/v2/models/Appearance.kt | 9 +++++++ 3 files changed, 39 insertions(+), 1 deletion(-) create mode 100644 src/main/kotlin/com/coder/toolbox/sdk/v2/models/Appearance.kt diff --git a/src/main/kotlin/com/coder/toolbox/sdk/CoderRestClient.kt b/src/main/kotlin/com/coder/toolbox/sdk/CoderRestClient.kt index 1ded07a..58b5837 100644 --- a/src/main/kotlin/com/coder/toolbox/sdk/CoderRestClient.kt +++ b/src/main/kotlin/com/coder/toolbox/sdk/CoderRestClient.kt @@ -10,6 +10,7 @@ import com.coder.toolbox.sdk.ex.APIResponseException import com.coder.toolbox.sdk.interceptors.Interceptors import com.coder.toolbox.sdk.v2.CoderV2RestFacade import com.coder.toolbox.sdk.v2.models.ApiErrorResponse +import com.coder.toolbox.sdk.v2.models.Appearance import com.coder.toolbox.sdk.v2.models.BuildInfo import com.coder.toolbox.sdk.v2.models.CreateWorkspaceBuildRequest import com.coder.toolbox.sdk.v2.models.Template @@ -45,6 +46,7 @@ open class CoderRestClient( lateinit var me: User lateinit var buildVersion: String + lateinit var appName: String init { setupSession() @@ -94,6 +96,7 @@ open class CoderRestClient( suspend fun initializeSession(): User { me = me() buildVersion = buildInfo().version + appName = appearance().applicationName return me } @@ -101,7 +104,7 @@ open class CoderRestClient( * Retrieve the current user. * @throws [APIResponseException]. */ - suspend fun me(): User { + internal suspend fun me(): User { val userResponse = retroRestClient.me() if (!userResponse.isSuccessful) { throw APIResponseException( @@ -117,6 +120,25 @@ open class CoderRestClient( } } + /** + * Retrieves the visual dashboard configuration. + */ + internal suspend fun appearance(): Appearance { + val appearanceResponse = retroRestClient.appearance() + if (!appearanceResponse.isSuccessful) { + throw APIResponseException( + "initializeSession", + url, + appearanceResponse.code(), + appearanceResponse.parseErrorBody(moshi) + ) + } + + return requireNotNull(appearanceResponse.body()) { + "Successful response returned null body or visual dashboard configuration" + } + } + /** * Retrieves the available workspaces created by the user. * @throws [APIResponseException]. diff --git a/src/main/kotlin/com/coder/toolbox/sdk/v2/CoderV2RestFacade.kt b/src/main/kotlin/com/coder/toolbox/sdk/v2/CoderV2RestFacade.kt index adcaa6e..5e7fc13 100644 --- a/src/main/kotlin/com/coder/toolbox/sdk/v2/CoderV2RestFacade.kt +++ b/src/main/kotlin/com/coder/toolbox/sdk/v2/CoderV2RestFacade.kt @@ -1,5 +1,6 @@ package com.coder.toolbox.sdk.v2 +import com.coder.toolbox.sdk.v2.models.Appearance import com.coder.toolbox.sdk.v2.models.BuildInfo import com.coder.toolbox.sdk.v2.models.CreateWorkspaceBuildRequest import com.coder.toolbox.sdk.v2.models.Template @@ -23,6 +24,12 @@ interface CoderV2RestFacade { @GET("api/v2/users/me") suspend fun me(): Response + /** + * Returns the configuration of the visual dashboard. + */ + @GET("api/v2/appearance") + suspend fun appearance(): Response + /** * Retrieves all workspaces the authenticated user has access to. */ diff --git a/src/main/kotlin/com/coder/toolbox/sdk/v2/models/Appearance.kt b/src/main/kotlin/com/coder/toolbox/sdk/v2/models/Appearance.kt new file mode 100644 index 0000000..0c8d830 --- /dev/null +++ b/src/main/kotlin/com/coder/toolbox/sdk/v2/models/Appearance.kt @@ -0,0 +1,9 @@ +package com.coder.toolbox.sdk.v2.models + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass + +@JsonClass(generateAdapter = true) +data class Appearance( + @property:Json(name = "application_name") val applicationName: String +) From 95801cf88fb1707d1ae6c232d41207589f27ea46 Mon Sep 17 00:00:00 2001 From: Faur Ioan-Aurel Date: Mon, 10 Nov 2025 23:56:38 +0200 Subject: [PATCH 4/9] impl: display the app name instead of the URL Handles both normal case and URI handling --- .../com/coder/toolbox/CoderRemoteProvider.kt | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/src/main/kotlin/com/coder/toolbox/CoderRemoteProvider.kt b/src/main/kotlin/com/coder/toolbox/CoderRemoteProvider.kt index 300f5a9..7eea26d 100644 --- a/src/main/kotlin/com/coder/toolbox/CoderRemoteProvider.kt +++ b/src/main/kotlin/com/coder/toolbox/CoderRemoteProvider.kt @@ -227,7 +227,7 @@ class CoderRemoteProvider( val url = context.settingsStore.workspaceCreateUrl ?: client?.url?.withPath("/templates").toString() context.desktop.browse( url - .replace("\$workspaceOwner", client?.me()?.username ?: "") + .replace("\$workspaceOwner", client?.me?.username ?: "") ) { context.ui.showErrorInfoPopup(it) } @@ -333,8 +333,11 @@ class CoderRemoteProvider( } context.logger.info("Starting initialization with the new settings") this@CoderRemoteProvider.client = restClient - coderHeaderPage.setTitle(context.i18n.pnotr(restClient.url.toString())) - + if (context.settingsStore.useAppNameAsTitle) { + coderHeaderPage.setTitle(context.i18n.pnotr(restClient.appName)) + } else { + coderHeaderPage.setTitle(context.i18n.pnotr(restClient.url.toString())) + } environments.showLoadingMessage() pollJob = poll(restClient, cli) context.logger.info("Workspace poll job with name ${pollJob.toString()} was created while handling URI $uri") @@ -421,7 +424,11 @@ class CoderRemoteProvider( context.logger.info("Cancelled workspace poll job ${pollJob.toString()} in order to start a new one") } environments.showLoadingMessage() - coderHeaderPage.setTitle(context.i18n.pnotr(client.url.toString())) + if (context.settingsStore.useAppNameAsTitle) { + coderHeaderPage.setTitle(context.i18n.pnotr(client.appName)) + } else { + coderHeaderPage.setTitle(context.i18n.pnotr(client.url.toString())) + } context.logger.info("Displaying ${client.url} in the UI") pollJob = poll(client, cli) context.logger.info("Workspace poll job with name ${pollJob.toString()} was created") From d468f4e843fefa4330c32810f04b32e128cbe4fa Mon Sep 17 00:00:00 2001 From: Faur Ioan-Aurel Date: Mon, 10 Nov 2025 23:58:57 +0200 Subject: [PATCH 5/9] chore: update changelog --- CHANGELOG.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8817ffb..35e430f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,10 @@ ## Unreleased +### Added + +- application name can now be displayed as the main title page instead of the URL + ## 0.7.2 - 2025-11-03 ### Changed From 3e6de8783509af3b2305aad084679018d6dadca7 Mon Sep 17 00:00:00 2001 From: Faur Ioan-Aurel Date: Tue, 11 Nov 2025 00:20:33 +0200 Subject: [PATCH 6/9] fix: don't cache the `useAppNameAsTitle` option Always read it from the underlying store. The way it is declared right now we read the value once and cache it. --- src/main/kotlin/com/coder/toolbox/store/CoderSettingsStore.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/kotlin/com/coder/toolbox/store/CoderSettingsStore.kt b/src/main/kotlin/com/coder/toolbox/store/CoderSettingsStore.kt index e4ae45b..ed8f009 100644 --- a/src/main/kotlin/com/coder/toolbox/store/CoderSettingsStore.kt +++ b/src/main/kotlin/com/coder/toolbox/store/CoderSettingsStore.kt @@ -38,7 +38,7 @@ class CoderSettingsStore( // Properties implementation override val lastDeploymentURL: String? get() = store[LAST_USED_URL] override val defaultURL: String get() = store[DEFAULT_URL] ?: "https://dev.coder.com" - override val useAppNameAsTitle: Boolean = store[APP_NAME_AS_TITLE]?.toBooleanStrictOrNull() ?: false + override val useAppNameAsTitle: Boolean get() = store[APP_NAME_AS_TITLE]?.toBooleanStrictOrNull() ?: false override val binarySource: String? get() = store[BINARY_SOURCE] override val binaryDirectory: String? get() = store[BINARY_DIRECTORY] override val disableSignatureVerification: Boolean From cb8396af0ac6303008eda9cccd05f5dfd6ff8527 Mon Sep 17 00:00:00 2001 From: Faur Ioan-Aurel Date: Tue, 11 Nov 2025 00:21:59 +0200 Subject: [PATCH 7/9] fix: update the title when settings page is closed A restart was needed in order for the title bar to be updated. With this patch the main page title is updated as soon as the user leaves the Settings page. --- .../kotlin/com/coder/toolbox/CoderRemoteProvider.kt | 11 ++++++++++- .../com/coder/toolbox/views/CoderSettingsPage.kt | 7 ++++++- 2 files changed, 16 insertions(+), 2 deletions(-) diff --git a/src/main/kotlin/com/coder/toolbox/CoderRemoteProvider.kt b/src/main/kotlin/com/coder/toolbox/CoderRemoteProvider.kt index 7eea26d..6084880 100644 --- a/src/main/kotlin/com/coder/toolbox/CoderRemoteProvider.kt +++ b/src/main/kotlin/com/coder/toolbox/CoderRemoteProvider.kt @@ -57,7 +57,6 @@ class CoderRemoteProvider( private val triggerSshConfig = Channel(Channel.CONFLATED) private val triggerProviderVisible = Channel(Channel.CONFLATED) - private val settingsPage: CoderSettingsPage = CoderSettingsPage(context, triggerSshConfig) private val dialogUi = DialogUi(context) // The REST client, if we are signed in @@ -65,8 +64,18 @@ class CoderRemoteProvider( // On the first load, automatically log in if we can. private var firstRun = true + private val isInitialized: MutableStateFlow = MutableStateFlow(false) private val coderHeaderPage = NewEnvironmentPage(context.i18n.pnotr(context.deploymentUrl.toString())) + private val settingsPage: CoderSettingsPage = CoderSettingsPage(context, triggerSshConfig) { + client?.let { restClient -> + if (context.settingsStore.useAppNameAsTitle) { + coderHeaderPage.setTitle(context.i18n.pnotr(restClient.appName)) + } else { + coderHeaderPage.setTitle(context.i18n.pnotr(restClient.url.toString())) + } + } + } private val visibilityState = MutableStateFlow( ProviderVisibilityState( applicationVisible = false, diff --git a/src/main/kotlin/com/coder/toolbox/views/CoderSettingsPage.kt b/src/main/kotlin/com/coder/toolbox/views/CoderSettingsPage.kt index 99bb3ad..b74b2d8 100644 --- a/src/main/kotlin/com/coder/toolbox/views/CoderSettingsPage.kt +++ b/src/main/kotlin/com/coder/toolbox/views/CoderSettingsPage.kt @@ -28,7 +28,11 @@ import kotlinx.coroutines.launch * TODO@JB: There is no scroll, and our settings do not fit. As a consequence, * I have not been able to test this page. */ -class CoderSettingsPage(private val context: CoderToolboxContext, triggerSshConfig: Channel) : +class CoderSettingsPage( + private val context: CoderToolboxContext, + triggerSshConfig: Channel, + private val onSettingsClosed: () -> Unit +) : CoderPage(MutableStateFlow(context.i18n.ptrl("Coder Settings")), false) { private val settings = context.settingsStore.readOnly() @@ -232,5 +236,6 @@ class CoderSettingsPage(private val context: CoderToolboxContext, triggerSshConf override fun afterHide() { visibilityUpdateJob.cancel() + onSettingsClosed() } } From 5224d87f1770b65e205322cbfa0ee5aeecf615b2 Mon Sep 17 00:00:00 2001 From: Faur Ioan-Aurel Date: Tue, 11 Nov 2025 00:28:12 +0200 Subject: [PATCH 8/9] chore: fix UT --- .../kotlin/com/coder/toolbox/util/CoderProtocolHandlerTest.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/test/kotlin/com/coder/toolbox/util/CoderProtocolHandlerTest.kt b/src/test/kotlin/com/coder/toolbox/util/CoderProtocolHandlerTest.kt index 4a9ef88..1a84061 100644 --- a/src/test/kotlin/com/coder/toolbox/util/CoderProtocolHandlerTest.kt +++ b/src/test/kotlin/com/coder/toolbox/util/CoderProtocolHandlerTest.kt @@ -60,7 +60,7 @@ internal class CoderProtocolHandlerTest { private val protocolHandler = CoderProtocolHandler( context, DialogUi(context), - CoderSettingsPage(context, Channel(Channel.CONFLATED)), + CoderSettingsPage(context, Channel(Channel.CONFLATED), {}), MutableStateFlow(ProviderVisibilityState(applicationVisible = true, providerVisible = true)), MutableStateFlow(false) ) From 896423422e0c3e61a28bf2ac85032cdaf929f581 Mon Sep 17 00:00:00 2001 From: Faur Ioan-Aurel Date: Wed, 12 Nov 2025 23:53:19 +0200 Subject: [PATCH 9/9] chore: fix typo --- src/main/kotlin/com/coder/toolbox/sdk/CoderRestClient.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/kotlin/com/coder/toolbox/sdk/CoderRestClient.kt b/src/main/kotlin/com/coder/toolbox/sdk/CoderRestClient.kt index 58b5837..d4117db 100644 --- a/src/main/kotlin/com/coder/toolbox/sdk/CoderRestClient.kt +++ b/src/main/kotlin/com/coder/toolbox/sdk/CoderRestClient.kt @@ -135,7 +135,7 @@ open class CoderRestClient( } return requireNotNull(appearanceResponse.body()) { - "Successful response returned null body or visual dashboard configuration" + "Successful response returned null body for visual dashboard configuration" } }