mirror of
https://github.com/Kilo-Org/kilocode.git
synced 2026-09-19 01:51:21 +08:00
fix(jetbrains): keep provider auth overlay fixed
This commit is contained in:
+4
-1
@@ -66,6 +66,7 @@ abstract class KiloReadyConfigurable : SearchableConfigurable, Configurable.NoSc
|
||||
override fun disposeUIResources() {
|
||||
val panel = ready
|
||||
val cs = scope
|
||||
if (panel is SettingsOverlayPanel) panel.setOverlayHost(null)
|
||||
shell = null
|
||||
scope = null
|
||||
ready = null
|
||||
@@ -94,7 +95,9 @@ abstract class KiloReadyConfigurable : SearchableConfigurable, Configurable.NoSc
|
||||
val cs = scope ?: return
|
||||
val panel = createReadyComponent(cs)
|
||||
ready = panel
|
||||
shell?.setContent(panel)
|
||||
val root = shell
|
||||
if (panel is SettingsOverlayPanel) panel.setOverlayHost(root)
|
||||
root?.setContent(panel)
|
||||
onReadyComponentCreated(panel)
|
||||
}
|
||||
|
||||
|
||||
+47
-21
@@ -2,10 +2,12 @@ package ai.kilocode.client.settings.base
|
||||
|
||||
import ai.kilocode.client.ui.LayeredOverlayPanel
|
||||
import ai.kilocode.client.ui.UiStyle
|
||||
import com.intellij.util.concurrency.annotations.RequiresEdt
|
||||
import java.awt.Rectangle
|
||||
|
||||
internal open class SettingsOverlayPanel : LayeredOverlayPanel() {
|
||||
val progress = SettingsProgressOverlay()
|
||||
private var host: SettingsOverlayPanel? = null
|
||||
|
||||
init {
|
||||
addOverlay(progress) { pane, child ->
|
||||
@@ -19,31 +21,55 @@ internal open class SettingsOverlayPanel : LayeredOverlayPanel() {
|
||||
}
|
||||
}
|
||||
|
||||
fun showProgress(text: String) {
|
||||
progress.showProgress(text)
|
||||
syncOverlay()
|
||||
}
|
||||
|
||||
fun showProgress(text: String, cancelText: String, cancel: () -> Unit) {
|
||||
progress.showProgress(text, cancelText, cancel)
|
||||
syncOverlay()
|
||||
}
|
||||
|
||||
fun updateProgress(text: String) {
|
||||
progress.updateProgress(text)
|
||||
syncOverlay()
|
||||
}
|
||||
|
||||
fun showError(text: String) {
|
||||
progress.showError(text)
|
||||
syncOverlay()
|
||||
}
|
||||
|
||||
fun clearProgress() {
|
||||
@RequiresEdt
|
||||
fun setOverlayHost(host: SettingsOverlayPanel?) {
|
||||
if (host === this) {
|
||||
this.host = null
|
||||
return
|
||||
}
|
||||
this.host?.clearProgress()
|
||||
this.host = host
|
||||
progress.clearProgress()
|
||||
syncOverlay()
|
||||
}
|
||||
|
||||
@RequiresEdt
|
||||
fun showProgress(text: String) {
|
||||
val panel = target()
|
||||
panel.progress.showProgress(text)
|
||||
panel.syncOverlay()
|
||||
}
|
||||
|
||||
@RequiresEdt
|
||||
fun showProgress(text: String, cancelText: String, cancel: () -> Unit) {
|
||||
val panel = target()
|
||||
panel.progress.showProgress(text, cancelText, cancel)
|
||||
panel.syncOverlay()
|
||||
}
|
||||
|
||||
@RequiresEdt
|
||||
fun updateProgress(text: String) {
|
||||
val panel = target()
|
||||
panel.progress.updateProgress(text)
|
||||
panel.syncOverlay()
|
||||
}
|
||||
|
||||
@RequiresEdt
|
||||
fun showError(text: String) {
|
||||
val panel = target()
|
||||
panel.progress.showError(text)
|
||||
panel.syncOverlay()
|
||||
}
|
||||
|
||||
@RequiresEdt
|
||||
fun clearProgress() {
|
||||
val panel = target()
|
||||
panel.progress.clearProgress()
|
||||
panel.syncOverlay()
|
||||
}
|
||||
|
||||
private fun target(): SettingsOverlayPanel = host ?: this
|
||||
|
||||
private fun syncOverlay() {
|
||||
overlay.revalidate()
|
||||
overlay.repaint()
|
||||
|
||||
+3
@@ -29,6 +29,7 @@ internal class SettingsProgressOverlay : JPanel(BorderLayout(UiStyle.Gap.md(), 0
|
||||
button.isVisible = false
|
||||
isVisible = false
|
||||
syncColors()
|
||||
UiStyle.Components.actionButton(button)
|
||||
}
|
||||
|
||||
fun showProgress(text: String) {
|
||||
@@ -72,6 +73,7 @@ internal class SettingsProgressOverlay : JPanel(BorderLayout(UiStyle.Gap.md(), 0
|
||||
}
|
||||
button.text = text
|
||||
button.addActionListener { action() }
|
||||
UiStyle.Components.actionButton(button)
|
||||
button.isVisible = true
|
||||
}
|
||||
|
||||
@@ -88,6 +90,7 @@ internal class SettingsProgressOverlay : JPanel(BorderLayout(UiStyle.Gap.md(), 0
|
||||
override fun updateUI() {
|
||||
super.updateUI()
|
||||
syncColors()
|
||||
cancel?.let { UiStyle.Components.actionButton(it) }
|
||||
}
|
||||
|
||||
private fun syncColors() {
|
||||
|
||||
+15
-17
@@ -22,7 +22,6 @@ import javax.swing.JList
|
||||
import javax.swing.JPanel
|
||||
import javax.swing.ListCellRenderer
|
||||
import javax.swing.SwingConstants
|
||||
import javax.swing.UIManager
|
||||
|
||||
private const val ACTION_GAP = 8
|
||||
|
||||
@@ -93,25 +92,27 @@ internal class ProviderListRenderer(
|
||||
private val mark = icon.align(HAlign.CENTER, VAlign.TOP)
|
||||
private val title = SimpleColoredComponent()
|
||||
private val desc = JBLabel()
|
||||
private val text = JPanel(BorderLayout()).apply {
|
||||
add(title, BorderLayout.NORTH)
|
||||
add(desc, BorderLayout.SOUTH)
|
||||
}
|
||||
private val text = Stack.vertical().next(title).next(desc)
|
||||
private val actions = Stack.horizontal(JBUI.scale(ACTION_GAP))
|
||||
private val row = Stack.horizontal(UiStyle.Gap.md()).next(mark).next(text)
|
||||
private val actionPane = actions.align(HAlign.RIGHT, VAlign.CENTER)
|
||||
private val row = JPanel(BorderLayout(UiStyle.Gap.md(), 0)).apply {
|
||||
add(mark, BorderLayout.WEST)
|
||||
add(text, BorderLayout.CENTER)
|
||||
add(actionPane, BorderLayout.EAST)
|
||||
}
|
||||
private val wrap = PickerRow()
|
||||
|
||||
init {
|
||||
isOpaque = true
|
||||
top.isOpaque = true
|
||||
UiStyle.Components.transparent(row, mark, icon, title, text, desc, actions)
|
||||
UiStyle.Components.transparent(row, mark, icon, title, text, desc, actions, actionPane)
|
||||
row.border = JBUI.Borders.empty(
|
||||
UiStyle.Gap.md(),
|
||||
UiStyle.Gap.lg(),
|
||||
UiStyle.Gap.md(),
|
||||
UiStyle.Gap.pad(),
|
||||
)
|
||||
wrap.setContent(row, actions)
|
||||
wrap.setContent(row)
|
||||
add(top, BorderLayout.NORTH)
|
||||
add(wrap, BorderLayout.CENTER)
|
||||
}
|
||||
@@ -145,12 +146,13 @@ internal class ProviderListRenderer(
|
||||
desc.foreground = weak
|
||||
|
||||
actions.removeAll()
|
||||
for (action in visibleActions(value, selected)) {
|
||||
val visible = visibleActions(value, selected)
|
||||
actions.isVisible = visible.isNotEmpty()
|
||||
actionPane.isVisible = visible.isNotEmpty()
|
||||
for (action in visible) {
|
||||
actions.add(ActionLabel(action).apply {
|
||||
isEnabled = value.enabled(action)
|
||||
foreground = if (isEnabled) UIManager.getColor("Button.foreground") ?: UIUtil.getLabelForeground()
|
||||
else UIManager.getColor("Button.disabledText") ?: UIUtil.getContextHelpForeground()
|
||||
background = UIManager.getColor("Button.background")
|
||||
UiStyle.Components.actionLabel(this, isEnabled)
|
||||
})
|
||||
}
|
||||
top.invalidate()
|
||||
@@ -168,11 +170,7 @@ internal class ProviderListRenderer(
|
||||
private class ActionLabel(action: ProviderListAction) : JBLabel(text(action)) {
|
||||
init {
|
||||
horizontalAlignment = SwingConstants.CENTER
|
||||
border = JBUI.Borders.compound(
|
||||
JBUI.Borders.customLine(UIUtil.getBoundsColor()),
|
||||
JBUI.Borders.empty(UiStyle.Gap.sm(), UiStyle.Gap.pad()),
|
||||
)
|
||||
isOpaque = true
|
||||
UiStyle.Components.actionLabel(this)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+1
-1
@@ -225,7 +225,7 @@ internal class ProvidersSettingsUi(
|
||||
withContext(edt) {
|
||||
if (!active(id)) return@withContext
|
||||
setBusy(false)
|
||||
showError("${e::class.simpleName}: ${e.message}")
|
||||
clearProgress()
|
||||
}
|
||||
} catch (e: CancellationException) {
|
||||
LOG.info("provider settings ui $name: coroutine cancelled durationMs=${System.currentTimeMillis() - start}")
|
||||
|
||||
@@ -6,6 +6,7 @@ import com.intellij.util.ui.JBFont
|
||||
import com.intellij.util.ui.JBUI
|
||||
import com.intellij.util.ui.UIUtil
|
||||
import java.awt.Color
|
||||
import javax.swing.AbstractButton
|
||||
import javax.swing.JComponent
|
||||
import javax.swing.UIManager
|
||||
|
||||
@@ -197,5 +198,35 @@ object UiStyle {
|
||||
fun transparent(vararg components: JComponent) {
|
||||
components.forEach { it.isOpaque = false }
|
||||
}
|
||||
|
||||
fun actionForeground(enabled: Boolean): Color = if (enabled) {
|
||||
UIManager.getColor("Button.foreground") ?: UIUtil.getLabelForeground()
|
||||
} else {
|
||||
UIManager.getColor("Button.disabledText") ?: UIUtil.getContextHelpForeground()
|
||||
}
|
||||
|
||||
fun actionBackground(): Color = UIManager.getColor("Button.background") ?: UIUtil.getPanelBackground()
|
||||
|
||||
fun actionBorder() = JBUI.Borders.compound(
|
||||
JBUI.Borders.customLine(UIUtil.getBoundsColor()),
|
||||
JBUI.Borders.empty(Gap.sm(), Gap.pad()),
|
||||
)
|
||||
|
||||
fun actionLabel(component: JComponent, enabled: Boolean = component.isEnabled) {
|
||||
component.foreground = actionForeground(enabled)
|
||||
component.background = actionBackground()
|
||||
component.border = actionBorder()
|
||||
component.isOpaque = true
|
||||
}
|
||||
|
||||
fun actionButton(button: AbstractButton) {
|
||||
button.foreground = actionForeground(button.isEnabled)
|
||||
button.background = actionBackground()
|
||||
button.border = actionBorder()
|
||||
button.isOpaque = true
|
||||
button.isBorderPainted = true
|
||||
button.isContentAreaFilled = false
|
||||
button.isFocusPainted = false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+36
-3
@@ -126,8 +126,33 @@ class KiloReadyConfigurableTest : BasePlatformTestCase() {
|
||||
assertTrue(view.disposedOnEdt)
|
||||
}
|
||||
|
||||
private fun create(): FakeConfigurable {
|
||||
val view = FakeConfigurable()
|
||||
fun `test ready settings overlay is hosted by outer shell`() {
|
||||
val view = create(overlay = true)
|
||||
val root = edt { view.createComponent() as SettingsPanel }
|
||||
flushUntil { rpc.connected }
|
||||
|
||||
rpc.state.value = KiloAppStateDto(KiloAppStatusDto.READY)
|
||||
flushUntil { edt { view.readyPanel != null } }
|
||||
|
||||
edt {
|
||||
val ready = requireNotNull(view.readyPanel)
|
||||
ready.showProgress("Authorizing provider")
|
||||
|
||||
assertTrue(text(root).contains("Authorizing provider"))
|
||||
assertTrue(root.progress.isVisible)
|
||||
assertFalse(ready.progress.isVisible)
|
||||
assertTrue(root.overlay.components.any { it === root.progress })
|
||||
assertFalse(ready.overlay.components.any { it === root.progress })
|
||||
}
|
||||
|
||||
edt { view.disposeUIResources() }
|
||||
cfg = null
|
||||
|
||||
edt { assertFalse(root.progress.isVisible) }
|
||||
}
|
||||
|
||||
private fun create(overlay: Boolean = false): FakeConfigurable {
|
||||
val view = FakeConfigurable(overlay)
|
||||
cfg = view
|
||||
return view
|
||||
}
|
||||
@@ -175,9 +200,11 @@ class KiloReadyConfigurableTest : BasePlatformTestCase() {
|
||||
visit(root)
|
||||
}
|
||||
|
||||
private class FakeConfigurable : KiloReadyConfigurable() {
|
||||
private class FakeConfigurable(private val overlay: Boolean = false) : KiloReadyConfigurable() {
|
||||
val field = JPanel()
|
||||
val focuses = mutableListOf<String>()
|
||||
var readyPanel: SettingsPanel? = null
|
||||
private set
|
||||
var created = 0
|
||||
private set
|
||||
var disposed = 0
|
||||
@@ -196,6 +223,12 @@ class KiloReadyConfigurableTest : BasePlatformTestCase() {
|
||||
|
||||
override fun createReadyComponent(cs: CoroutineScope): JComponent {
|
||||
created++
|
||||
if (overlay) {
|
||||
val panel = SettingsPanel()
|
||||
panel.setContent(JPanel().apply { add(JLabel("Ready content")) })
|
||||
readyPanel = panel
|
||||
return panel
|
||||
}
|
||||
return JPanel().apply { add(JLabel("Ready content")) }
|
||||
}
|
||||
|
||||
|
||||
+2
@@ -119,6 +119,8 @@ class SettingsRowsTest : BasePlatformTestCase() {
|
||||
assertFalse(text(panel.overlay).contains("Sign in to Kilo Code"))
|
||||
assertTrue(panel.overlay.components.any { it === panel.progress })
|
||||
assertTrue(text(panel.progress).contains("Loading models..."))
|
||||
val scroll = components(panel.content).filterIsInstance<JScrollPane>().single()
|
||||
assertFalse(components(scroll.viewport.view as JComponent).any { it === panel.progress })
|
||||
}
|
||||
|
||||
fun `test settings panel tracks viewport width without horizontal scroll`() {
|
||||
|
||||
+61
@@ -2,6 +2,7 @@ package ai.kilocode.client.settings.providers
|
||||
|
||||
import ai.kilocode.client.app.KiloProviderService
|
||||
import ai.kilocode.client.testing.FakeProviderRpcApi
|
||||
import ai.kilocode.client.ui.UiStyle
|
||||
import ai.kilocode.rpc.dto.CustomProviderConfigDto
|
||||
import ai.kilocode.rpc.dto.ModelDto
|
||||
import ai.kilocode.rpc.dto.ProviderAuthMethodDto
|
||||
@@ -21,10 +22,12 @@ import com.intellij.util.ui.UIUtil
|
||||
import kotlinx.coroutines.CompletableDeferred
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.SupervisorJob
|
||||
import kotlinx.coroutines.TimeoutCancellationException
|
||||
import kotlinx.coroutines.cancel
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import kotlinx.coroutines.withContext
|
||||
import kotlinx.coroutines.withTimeout
|
||||
import java.awt.BorderLayout
|
||||
import java.awt.Container
|
||||
import java.awt.Dimension
|
||||
@@ -327,6 +330,24 @@ class ProvidersSettingsUiTest : BasePlatformTestCase() {
|
||||
}
|
||||
}
|
||||
|
||||
fun `test renderer lays out action labels with visible bounds`() {
|
||||
edt {
|
||||
val row = ProviderListRow(provider("openai", "OpenAI"), "Popular providers", listOf(ProviderListAction.CONNECT))
|
||||
val list = JBList(listOf(row))
|
||||
val renderer = ProviderListRenderer(com.intellij.ui.CollectionListModel(listOf(row)))
|
||||
|
||||
renderer.getListCellRendererComponent(list, row, 0, true, false)
|
||||
renderer.setSize(320, renderer.preferredSize.height)
|
||||
renderer.doLayout()
|
||||
components(renderer).filterIsInstance<Container>().forEach { it.doLayout() }
|
||||
|
||||
val label = components(renderer).filterIsInstance<JBLabel>().single { it.text == "Connect" }
|
||||
assertTrue(label.isShowing || label.isVisible)
|
||||
assertTrue(label.width > 0)
|
||||
assertTrue(label.height > 0)
|
||||
}
|
||||
}
|
||||
|
||||
fun `test renderer hides unselected unconnected action labels`() {
|
||||
edt {
|
||||
val row = ProviderListRow(provider("cloudflare", "Cloudflare"), "All providers", listOf(ProviderListAction.CONNECT))
|
||||
@@ -437,6 +458,7 @@ class ProvidersSettingsUiTest : BasePlatformTestCase() {
|
||||
val area = ProviderListRenderer.actionBounds(list, bounds, row, selected = true).getValue(ProviderListAction.CONNECT)
|
||||
|
||||
assertTrue(kotlin.math.abs((bounds.y + bounds.height / 2) - (area.y + area.height / 2)) <= 1)
|
||||
assertTrue(bounds.contains(area))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -489,6 +511,10 @@ class ProvidersSettingsUiTest : BasePlatformTestCase() {
|
||||
|
||||
edt {
|
||||
assertTrue(text(panel).contains("Cancel"))
|
||||
val cancel = components(panel).filterIsInstance<JButton>().single { it.text == "Cancel" && it.isVisible }
|
||||
assertEquals(UiStyle.Components.actionForeground(true), cancel.foreground)
|
||||
assertEquals(UiStyle.Components.actionBackground(), cancel.background)
|
||||
assertEquals(requireNotNull(UiStyle.Components.actionBorder()).getBorderInsets(cancel), requireNotNull(cancel.border).getBorderInsets(cancel))
|
||||
assertTrue(rows(panel).single().disabled)
|
||||
assertTrue(ProviderListRenderer.visibleActions(rows(panel).single(), selected = true).isEmpty())
|
||||
panel.reload()
|
||||
@@ -551,6 +577,41 @@ class ProvidersSettingsUiTest : BasePlatformTestCase() {
|
||||
}
|
||||
}
|
||||
|
||||
fun `test provider oauth timeout clears progress without error`() {
|
||||
val ready = CompletableDeferred<ProviderOAuthReadyDto>()
|
||||
val rpc = installProvider(
|
||||
ProviderSettingsDto(
|
||||
providers = listOf(provider("github-copilot", "GitHub Copilot")),
|
||||
auth = mapOf("github-copilot" to listOf(ProviderAuthMethodDto("oauth", "OAuth"))),
|
||||
),
|
||||
)
|
||||
rpc.authorizesReady.add(ready)
|
||||
val panel = edt { createUi() }
|
||||
|
||||
flushUntil { rpc.stateCalls.size == 1 && edt { rows(panel).map { it.key } == listOf("github-copilot") } }
|
||||
edt { triggerPrimary(panel) }
|
||||
flushUntil { rpc.authorizes.size == 1 && edt { text(panel).contains("Starting OAuth for GitHub Copilot") } }
|
||||
val timeout = runBlocking {
|
||||
try {
|
||||
withTimeout(1) { delay(Long.MAX_VALUE) }
|
||||
error("timeout expected")
|
||||
} catch (e: TimeoutCancellationException) {
|
||||
e
|
||||
}
|
||||
}
|
||||
ready.completeExceptionally(timeout)
|
||||
|
||||
flushUntil { edt { rows(panel).single().disabled.not() && !text(panel).contains("Starting OAuth") } }
|
||||
|
||||
edt {
|
||||
val visible = text(panel)
|
||||
assertFalse(visible.contains("TimeoutCancellationException"))
|
||||
assertFalse(visible.contains("OAuth timed out"))
|
||||
assertFalse(visible.contains("Cancel"))
|
||||
assertTrue(rpc.callbacks.isEmpty())
|
||||
}
|
||||
}
|
||||
|
||||
fun `test provider action failure returns error state`() = runBlocking {
|
||||
val cs = CoroutineScope(SupervisorJob())
|
||||
scope = cs
|
||||
|
||||
Reference in New Issue
Block a user