fix(jetbrains): hide account overlay when not logged in

This commit is contained in:
kirillk
2026-05-19 14:29:20 -04:00
parent 09572d3d47
commit 5fc0829ee8
4 changed files with 36 additions and 100 deletions
@@ -0,0 +1,5 @@
---
"@kilocode/kilo-jetbrains": patch
---
Show the logged-out account status in the same rounded panel as the logged-in account overlay, with a "Not logged in" label, hidden picker/balance, and a profile icon to open settings.
@@ -149,7 +149,6 @@ class SessionUi(
account = SessionAccountOverlay(
select = { org -> controller.selectOrganization(org) },
login = { controller.openProfile() },
profile = { controller.openProfile() },
)
root.addOverlay(account) { pane, child ->
@@ -18,17 +18,13 @@ import com.intellij.ui.components.JBList
import com.intellij.util.ui.JBUI
import com.intellij.util.ui.UIUtil
import com.intellij.util.ui.components.BorderLayoutPanel
import java.awt.CardLayout
import java.awt.Cursor
import java.awt.GridBagConstraints
import java.awt.GridBagLayout
import java.awt.event.KeyEvent
import java.awt.event.MouseAdapter
import java.awt.event.MouseEvent
import java.text.DecimalFormat
import javax.swing.Box
import javax.swing.BoxLayout
import javax.swing.JButton
import javax.swing.JComponent
import javax.swing.JPanel
import javax.swing.KeyStroke
@@ -38,38 +34,14 @@ import javax.swing.ScrollPaneConstants
/**
* Compact account overlay shown in the top-right of the empty session screen.
*
* Displays logged-out prompt or logged-in account/balance info.
* Only visible when logged in. Hidden when not logged in or no profile is available.
* Visibility is controlled entirely by [onEvent] — never set [isVisible] externally.
*/
internal class SessionAccountOverlay(
private val select: (String?) -> Unit,
private val login: () -> Unit,
private val profile: () -> Unit,
) : BorderLayoutPanel() {
companion object {
private const val CARD_OUT = "out"
private const val CARD_IN = "in"
}
private val loginLabel = JBLabel(KiloBundle.message("profile.notLoggedIn")).apply {
foreground = UiStyle.Colors.weak()
}
private val loginBtn = JButton(KiloBundle.message("profile.action.login")).apply {
isOpaque = false
addActionListener { login() }
}
private val outCard = JPanel(GridBagLayout()).apply {
isOpaque = false
add(loginLabel, GridBagConstraints().apply {
gridx = 0; gridy = 0; anchor = GridBagConstraints.WEST
})
add(loginBtn, GridBagConstraints().apply {
gridx = 0; gridy = 1; anchor = GridBagConstraints.CENTER
insets = JBUI.insetsTop(UiStyle.Gap.sm())
})
}
private val picker = PickerButton().apply {
isEnabled = false
text = " "
@@ -110,27 +82,13 @@ internal class SessionAccountOverlay(
addToCenter(row)
}
private val inCard = JPanel(GridBagLayout()).apply {
isOpaque = false
add(panel, GridBagConstraints().apply {
gridx = 0; gridy = 0; fill = GridBagConstraints.HORIZONTAL
})
}
private val cardLayout = CardLayout()
private val cards = JPanel(cardLayout).apply {
isOpaque = false
add(outCard, CARD_OUT)
add(inCard, CARD_IN)
}
private var choices: List<AccountChoice> = emptyList()
private var currentOrgId: String? = null
init {
isOpaque = false
isVisible = false
addToCenter(cards)
addToCenter(panel)
}
fun onEvent(event: SessionControllerEvent.AccountOverlayChanged) {
@@ -148,16 +106,13 @@ internal class SessionAccountOverlay(
val snap = event.account
val prof = snap.profile
if (prof == null) {
if (!snap.transient) {
layout = showCard(CARD_OUT) || layout
if (!isVisible) {
isVisible = true
layout = true
}
if (!snap.transient && isVisible) {
isVisible = false
layout = true
paint = true
}
} else {
layout = updateLoggedIn(prof, snap.switching, snap.targetOrgId) || layout
layout = showCard(CARD_IN) || layout
if (!isVisible) {
isVisible = true
layout = true
@@ -169,22 +124,9 @@ internal class SessionAccountOverlay(
if (layout || paint) repaint()
}
private fun activeCard(): String? {
for (i in 0 until cards.componentCount) {
val comp = cards.getComponent(i)
if (comp.isVisible) return if (comp === inCard) CARD_IN else CARD_OUT
}
return null
}
private fun showCard(card: String): Boolean {
if (activeCard() == card) return false
cardLayout.show(cards, card)
return true
}
private fun updateLoggedIn(prof: ai.kilocode.rpc.dto.ProfileDto, switching: Boolean, target: String?): Boolean {
var layout = false
val orgs = prof.organizations
val next = listOf(AccountChoice(null, KiloBundle.message("profile.personalAccount"))) +
orgs.map { org -> AccountChoice(org.id, org.name) }
@@ -216,11 +158,6 @@ internal class SessionAccountOverlay(
}
if (picker.toolTipText != tip) picker.toolTipText = tip
if (!picker.isVisible) {
picker.isVisible = true
layout = true
}
layout = syncBalance(prof) || layout
return layout
}
@@ -333,23 +270,7 @@ internal class SessionAccountOverlay(
popup.showUnderneathOf(picker)
}
internal fun loggedInVisible() = isVisible && cards.let {
var card = CARD_OUT
for (i in 0 until it.componentCount) {
val comp = it.getComponent(i)
if (comp.isVisible) card = if (comp === inCard) CARD_IN else CARD_OUT
}
card == CARD_IN
}
internal fun loggedOutVisible() = isVisible && cards.let {
for (i in 0 until it.componentCount) {
val comp = it.getComponent(i)
if (comp.isVisible) return@let comp === outCard
}
false
}
internal fun loggedInVisible() = isVisible
internal fun accountTitle(): String? = picker.text?.removeSuffix(" ▾")?.ifBlank { null }
internal fun pickerEnabled() = picker.isEnabled
internal fun pickerVisible() = picker.isVisible
@@ -15,15 +15,12 @@ import com.intellij.icons.AllIcons
class SessionAccountOverlayTest : SessionControllerTestBase() {
private lateinit var panel: SessionAccountOverlay
private val selected = mutableListOf<String?>()
private var loginCalls = 0
private var profileCalls = 0
override fun setUp() {
super.setUp()
panel = SessionAccountOverlay(
select = { org -> selected.add(org) },
login = { loginCalls++ },
select = { },
profile = { profileCalls++ },
)
}
@@ -56,13 +53,12 @@ class SessionAccountOverlayTest : SessionControllerTestBase() {
private fun org(id: String, name: String, role: String = "MEMBER") =
ProfileOrganizationDto(id = id, name = name, role = role)
// --- test 1: logged-out state shows login prompt ---
// --- test 1: logged-out state hides the overlay entirely ---
fun `test logged out state is visible with login button`() {
fun `test logged out state hides overlay`() {
show(snap(null))
assertTrue(panel.isVisible)
assertTrue(panel.loggedOutVisible())
assertFalse(panel.isVisible)
}
// --- test 2: logged-in personal account shows picker title ---
@@ -113,18 +109,22 @@ class SessionAccountOverlayTest : SessionControllerTestBase() {
// --- test 4: programmatic update does not call select callback ---
fun `test programmatic update does not call select callback`() {
val selected = mutableListOf<String?>()
val p = SessionAccountOverlay(
select = { org -> selected.add(org) },
profile = {},
)
val acme = org("org_1", "Acme")
val prof = profile(
email = "user@example.com",
organizations = listOf(acme),
currentOrgId = null,
)
// Show with personal account selected
show(snap(prof))
edt { p.onEvent(SessionControllerEvent.AccountOverlayChanged.Show(snap(prof))) }
selected.clear()
// Show again with same profile - no user selection
show(snap(prof))
edt { p.onEvent(SessionControllerEvent.AccountOverlayChanged.Show(snap(prof))) }
assertEquals(0, selected.size)
}
@@ -226,7 +226,7 @@ class SessionAccountOverlayTest : SessionControllerTestBase() {
)
show(transientSnap)
// Should remain visible and logged-in, not flash to logged-out
// Should remain visible and logged-in, not flash to hidden
assertTrue(panel.isVisible)
assertTrue(panel.loggedInVisible())
}
@@ -317,4 +317,15 @@ class SessionAccountOverlayTest : SessionControllerTestBase() {
assertTrue(panel.balanceVisible())
assertSame(icon, panel.balanceIcon())
}
// --- test 16: non-transient null profile after login hides overlay ---
fun `test non-transient null profile after login hides overlay`() {
show(snap(profile(email = "user@example.com")))
assertTrue(panel.isVisible)
show(snap(null))
assertFalse(panel.isVisible)
}
}