From 910f4a6c3d73dc5d7cdd375fc4eea884a3ef383b Mon Sep 17 00:00:00 2001 From: kirillk Date: Mon, 1 Jun 2026 18:23:32 -0400 Subject: [PATCH] fix(kilo-jetbrains): show sign-in banner in models settings --- .../settings/models/ModelsSettingsState.kt | 4 +- .../settings/models/ModelsSettingsUi.kt | 47 ++++++++++++++++++- .../resources/messages/KiloBundle.properties | 2 + .../models/ModelsSettingsStateTest.kt | 23 +++++++++ 4 files changed, 73 insertions(+), 3 deletions(-) diff --git a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/settings/models/ModelsSettingsState.kt b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/settings/models/ModelsSettingsState.kt index b7fa01d4027..b13fc9b8445 100644 --- a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/settings/models/ModelsSettingsState.kt +++ b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/settings/models/ModelsSettingsState.kt @@ -70,8 +70,10 @@ internal fun modelsStatus( if (saving) return ModelsStatus.SAVING if (!ready) return ModelsStatus.UNAVAILABLE if (loading) return ModelsStatus.LOADING - if (providers == null) return if (errors.isNotEmpty()) ModelsStatus.LOAD_FAILED else ModelsStatus.LOADING + if (providers == null) return ModelsStatus.LOAD_FAILED if (items == 0) return ModelsStatus.NO_PROVIDERS if (errors.any { it.resource == "agents" }) return ModelsStatus.MODES_FAILED return ModelsStatus.READY } + +internal fun modelsLoginBannerVisible(ready: Boolean, authenticated: Boolean): Boolean = ready && !authenticated diff --git a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/settings/models/ModelsSettingsUi.kt b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/settings/models/ModelsSettingsUi.kt index e52c8f03791..b0e282013f3 100644 --- a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/settings/models/ModelsSettingsUi.kt +++ b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/settings/models/ModelsSettingsUi.kt @@ -6,6 +6,7 @@ import ai.kilocode.client.plugin.KiloBundle import ai.kilocode.client.session.ui.ReasoningPicker import ai.kilocode.client.session.ui.model.ModelPicker import ai.kilocode.client.session.ui.model.ModelText +import ai.kilocode.client.settings.profile.UserProfileConfigurable import ai.kilocode.client.settings.profile.edt import ai.kilocode.client.settings.ui.SettingsRow import ai.kilocode.client.settings.ui.SettingsRows @@ -20,8 +21,16 @@ import ai.kilocode.rpc.dto.KiloAppStatusDto import ai.kilocode.rpc.dto.LoadErrorDto import ai.kilocode.rpc.dto.ModelsWorkspaceDto import ai.kilocode.rpc.dto.ProvidersDto +import com.intellij.ide.DataManager import com.intellij.openapi.application.ApplicationManager import com.intellij.openapi.components.service +import com.intellij.openapi.options.Configurable +import com.intellij.openapi.options.ConfigurableWithId +import com.intellij.openapi.options.ShowSettingsUtil +import com.intellij.openapi.options.ex.Settings +import com.intellij.openapi.project.ProjectManager +import com.intellij.ui.EditorNotificationPanel +import com.intellij.ui.InlineBanner import com.intellij.ui.TitledSeparator import com.intellij.ui.components.JBLabel import com.intellij.ui.components.JBScrollPane @@ -33,6 +42,8 @@ import kotlinx.coroutines.cancel import kotlinx.coroutines.launch import kotlinx.coroutines.withContext import java.awt.BorderLayout +import java.util.function.Predicate +import javax.swing.JComponent import javax.swing.JPanel internal class ModelsSettingsUi( @@ -43,6 +54,10 @@ internal class ModelsSettingsUi( ) : JPanel(BorderLayout()) { private val status = JBLabel(KiloBundle.message("settings.models.loading")) + private val banner = InlineBanner( + KiloBundle.message("settings.models.login.message"), + EditorNotificationPanel.Status.Warning, + ).showCloseButton(false) private val content = Stack.vertical() private val rows = SettingsRows() private val modes = SettingsRows() @@ -76,6 +91,9 @@ internal class ModelsSettingsUi( picker.picker.onFavoriteToggle = { app.toggleModelFavorite(it.provider, it.id) } } + banner.addAction(KiloBundle.message("settings.models.login.action"), Runnable { openProfile(banner) }) + content.next(banner) + content.gap(UiStyle.Gap.md()) content.next(status) content.gap(UiStyle.Gap.lg()) content.next(TitledSeparator(KiloBundle.message("settings.models.displayName"))) @@ -240,15 +258,20 @@ internal class ModelsSettingsUi( checkEdt() allItems = items(false) val smallItems = items(true) + val hasDir = dir != null || directory != null val state = modelsStatus( - ready = appState.status == KiloAppStatusDto.READY, - loading = loading || (appState.status == KiloAppStatusDto.READY && !loaded && dir != null), + ready = appState.status == KiloAppStatusDto.READY && hasDir, + loading = loading || (appState.status == KiloAppStatusDto.READY && !loaded && hasDir), providers = providers, items = allItems.size, errors = errors, saving = saving, ) val ready = state == ModelsStatus.READY || state == ModelsStatus.MODES_FAILED + banner.isVisible = modelsLoginBannerVisible( + ready = appState.status == KiloAppStatusDto.READY, + authenticated = appState.profile != null, + ) if (!saving) { when (state) { ModelsStatus.UNAVAILABLE -> { @@ -348,6 +371,26 @@ internal class ModelsSettingsUi( update { copy(subagent = item.key, variant = variant) } } + private fun openProfile(src: JComponent) { + val settings = Settings.KEY.getData(DataManager.getInstance().getDataContext(src)) + if (settings != null) { + val cfg = settings.find(UserProfileConfigurable.ID) + if (cfg != null) { + settings.select(cfg) + return + } + } + + val project = ProjectManager.getInstance().openProjects.firstOrNull { !it.isDefault } + ShowSettingsUtil.getInstance().showSettingsDialog( + project, + Predicate { cfg: Configurable -> + cfg is ConfigurableWithId && cfg.getId() == UserProfileConfigurable.ID + }, + { cfg: Configurable -> cfg.focusOn(UserProfileConfigurable.FOCUS_ACCOUNT_COMBO) }, + ) + } + @RequiresEdt private fun update(fn: ModelsDraft.() -> ModelsDraft) { checkEdt() diff --git a/packages/kilo-jetbrains/frontend/src/main/resources/messages/KiloBundle.properties b/packages/kilo-jetbrains/frontend/src/main/resources/messages/KiloBundle.properties index 84a7800851d..de0755a1ae2 100644 --- a/packages/kilo-jetbrains/frontend/src/main/resources/messages/KiloBundle.properties +++ b/packages/kilo-jetbrains/frontend/src/main/resources/messages/KiloBundle.properties @@ -207,6 +207,8 @@ settings.models.unavailable=Models are unavailable until Kilo finishes loading. settings.models.load.failed=Failed to load model settings settings.models.noProviders=No providers available settings.models.modes.failed=Failed to load per-mode settings +settings.models.login.message=Sign in to Kilo Code to access account-backed models and manage billing. +settings.models.login.action=Open User Profile settings.models.save.failed=Failed to save model settings settings.models.save.pending=Saving model settings... settings.models.reset=Reset diff --git a/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/settings/models/ModelsSettingsStateTest.kt b/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/settings/models/ModelsSettingsStateTest.kt index 6c778c02723..8a1b8255dde 100644 --- a/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/settings/models/ModelsSettingsStateTest.kt +++ b/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/settings/models/ModelsSettingsStateTest.kt @@ -7,7 +7,9 @@ import ai.kilocode.rpc.dto.LoadErrorDto import ai.kilocode.rpc.dto.ProvidersDto import kotlin.test.Test import kotlin.test.assertEquals +import kotlin.test.assertFalse import kotlin.test.assertNull +import kotlin.test.assertTrue class ModelsSettingsStateTest { @@ -96,6 +98,20 @@ class ModelsSettingsStateTest { assertEquals(ModelsStatus.LOAD_FAILED, status) } + @Test + fun `models status reports missing providers after loading completes`() { + val status = modelsStatus( + ready = true, + loading = false, + providers = null, + items = 0, + errors = emptyList(), + saving = false, + ) + + assertEquals(ModelsStatus.LOAD_FAILED, status) + } + @Test fun `models status reports app unavailable`() { val status = modelsStatus( @@ -109,4 +125,11 @@ class ModelsSettingsStateTest { assertEquals(ModelsStatus.UNAVAILABLE, status) } + + @Test + fun `login banner shows only when ready and unauthenticated`() { + assertTrue(modelsLoginBannerVisible(ready = true, authenticated = false)) + assertFalse(modelsLoginBannerVisible(ready = true, authenticated = true)) + assertFalse(modelsLoginBannerVisible(ready = false, authenticated = false)) + } }