From 20bae4f3d92c71129e49ff2f84a5d9a2393cd034 Mon Sep 17 00:00:00 2001 From: kirillk Date: Thu, 21 May 2026 10:26:23 -0400 Subject: [PATCH] fix(jetbrains): align session question views --- .changeset/session-question-view-style.md | 5 ++ .../ui/shared/BaseSessionQuestionPanel.kt | 43 ++++++++-- .../client/session/views/LoginRequiredView.kt | 13 ++- .../client/session/views/PermissionView.kt | 4 + .../views/question/QuestionResultView.kt | 9 +- .../session/views/question/QuestionView.kt | 10 ++- .../session/ui/SessionEditorStyleTest.kt | 18 ++++ .../ui/shared/BaseSessionQuestionPanelTest.kt | 82 ++++++++++++++++++- .../session/views/LoginRequiredViewTest.kt | 49 +++++++++++ .../session/views/PermissionViewTest.kt | 54 ++++++++++++ .../session/views/QuestionResultViewTest.kt | 23 +++++- .../client/session/views/QuestionViewTest.kt | 35 ++++++-- 12 files changed, 322 insertions(+), 23 deletions(-) create mode 100644 .changeset/session-question-view-style.md diff --git a/.changeset/session-question-view-style.md b/.changeset/session-question-view-style.md new file mode 100644 index 00000000000..ca25c743d45 --- /dev/null +++ b/.changeset/session-question-view-style.md @@ -0,0 +1,5 @@ +--- +"@kilocode/kilo-jetbrains": patch +--- + +Improve question-based session views so UI text uses editor-sized interface fonts, actions align consistently, and permission prompts show a header icon. diff --git a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/ui/shared/BaseSessionQuestionPanel.kt b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/ui/shared/BaseSessionQuestionPanel.kt index a91a586634b..ed36d799f32 100644 --- a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/ui/shared/BaseSessionQuestionPanel.kt +++ b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/ui/shared/BaseSessionQuestionPanel.kt @@ -5,14 +5,17 @@ import ai.kilocode.client.session.ui.style.SessionEditorStyleTarget import ai.kilocode.client.session.ui.style.SessionUiStyle import ai.kilocode.client.ui.RoundedContentPanel import ai.kilocode.client.ui.UiStyle +import com.intellij.ui.components.JBLabel import com.intellij.ui.components.JBTextArea import com.intellij.util.concurrency.annotations.RequiresEdt import com.intellij.util.ui.JBUI +import java.awt.BorderLayout import java.awt.Color import java.awt.Component import java.awt.Dimension import javax.swing.Box import javax.swing.BoxLayout +import javax.swing.Icon import javax.swing.JComponent import javax.swing.JPanel @@ -27,9 +30,9 @@ import javax.swing.JPanel * outer card shell so they share the same background, padding, and text * styling without duplicating the setup. * - * The column always contains (in order): optional top, [headerText], + * The column always contains (in order): optional top, header row with [headerText], * [descriptionText], optional body, optional footer. Call [setTopPanel], - * [setBody], or [setFooter] to replace those slots at any time. + * [setHeaderIcon], [setBody], or [setFooter] to replace those slots at any time. */ class BaseSessionQuestionPanel : RoundedContentPanel( UiStyle.Gap.lg(), @@ -38,7 +41,7 @@ class BaseSessionQuestionPanel : RoundedContentPanel( private var style = SessionEditorStyle.current() - // All JBTextArea instances that need editor-font updates, paired with bold flag + // All JBTextArea instances that need style updates, paired with bold flag private val tracked = mutableListOf>() // ---- header text ---- @@ -47,6 +50,23 @@ class BaseSessionQuestionPanel : RoundedContentPanel( // ---- description text ---- val descriptionText: JBTextArea = makeText("", UiStyle.Colors.weak(), bold = false) + private val icon = JBLabel().apply { + border = JBUI.Borders.emptyRight(UiStyle.Gap.sm()) + isVisible = false + } + + private val header = object : JPanel(BorderLayout(UiStyle.Gap.sm(), 0)) { + override fun getMaximumSize(): Dimension { + val size = preferredSize + return Dimension(Int.MAX_VALUE, size.height) + } + }.apply { + isOpaque = false + alignmentX = Component.LEFT_ALIGNMENT + add(icon, BorderLayout.WEST) + add(headerText, BorderLayout.CENTER) + } + // ---- slot fields ---- private var top: JComponent? = null private var body: JComponent? = null @@ -77,6 +97,19 @@ class BaseSessionQuestionPanel : RoundedContentPanel( rebuildCol() } + /** + * Optional icon rendered at the left edge of the header row. + * Pass `null` to remove the icon while keeping header text alignment stable. + */ + @RequiresEdt + fun setHeaderIcon(icon: Icon?, tooltip: String? = null) { + this.icon.icon = icon + this.icon.toolTipText = tooltip + this.icon.isVisible = icon != null + this.icon.revalidate() + this.icon.repaint() + } + /** * Replace the body slot that comes after the header/description. * Pass `null` to remove the current body. @@ -116,7 +149,7 @@ class BaseSessionQuestionPanel : RoundedContentPanel( private fun rebuildCol() { col.removeAll() top?.let { col.add(it) } - col.add(headerText) + col.add(header) col.add(descriptionText) body?.let { col.add(gap()) @@ -182,7 +215,7 @@ class BaseSessionQuestionPanel : RoundedContentPanel( } private fun applyFont(area: JBTextArea, bold: Boolean) { - val font = if (bold) style.boldEditorFont else style.transcriptFont + val font = if (bold) style.boldUiFont else style.uiFont if (area.font != font) area.font = font } } diff --git a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/views/LoginRequiredView.kt b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/views/LoginRequiredView.kt index 3e56e999632..a4d98ba2b01 100644 --- a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/views/LoginRequiredView.kt +++ b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/views/LoginRequiredView.kt @@ -7,10 +7,13 @@ import ai.kilocode.client.session.ui.shared.applyButton import ai.kilocode.client.session.ui.shared.dismissButton import ai.kilocode.client.session.ui.style.SessionEditorStyle import ai.kilocode.client.session.ui.style.SessionEditorStyleTarget +import ai.kilocode.client.ui.UiStyle import com.intellij.util.concurrency.annotations.RequiresEdt import com.intellij.util.ui.components.BorderLayoutPanel import java.awt.BorderLayout import java.awt.Component +import javax.swing.Box +import javax.swing.BoxLayout import javax.swing.JPanel /** @@ -43,9 +46,15 @@ class LoginRequiredView( val footer = JPanel(BorderLayout()).apply { isOpaque = false alignmentX = Component.LEFT_ALIGNMENT - add(dismissButton, BorderLayout.WEST) - add(openProfileButton, BorderLayout.EAST) } + val actions = JPanel().apply { + isOpaque = false + layout = BoxLayout(this, BoxLayout.X_AXIS) + add(dismissButton) + add(Box.createHorizontalStrut(UiStyle.Gap.sm())) + add(openProfileButton) + } + footer.add(actions, BorderLayout.EAST) card.setFooter(footer) diff --git a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/views/PermissionView.kt b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/views/PermissionView.kt index 78df4d1d464..119fe5f4ecb 100644 --- a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/views/PermissionView.kt +++ b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/views/PermissionView.kt @@ -14,6 +14,7 @@ import ai.kilocode.client.session.ui.style.SessionUiStyle import ai.kilocode.client.ui.UiStyle import ai.kilocode.client.ui.md.MdView import ai.kilocode.rpc.dto.PermissionReplyDto +import com.intellij.icons.AllIcons import com.intellij.ui.components.JBScrollPane import com.intellij.util.ui.JBUI import com.intellij.util.ui.components.BorderLayoutPanel @@ -75,6 +76,7 @@ class PermissionView( actions.add(run) footer.add(actions, BorderLayout.EAST) + card.setHeaderIcon(AllIcons.General.Warning, KiloBundle.message("session.permission.title")) card.setBody(body) card.setFooter(footer) addToCenter(card) @@ -239,6 +241,8 @@ class PermissionView( // Test helpers internal fun runButtonForTest() = run internal fun denyButtonForTest() = deny + internal fun firstCmdViewForTest() = cmdViews.firstOrNull() + internal fun headerFontForTest() = card.headerText.font } /** diff --git a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/views/question/QuestionResultView.kt b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/views/question/QuestionResultView.kt index db1994ef8dd..d78c9bb7802 100644 --- a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/views/question/QuestionResultView.kt +++ b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/views/question/QuestionResultView.kt @@ -108,7 +108,9 @@ class QuestionResultView(tool: Tool) : PartView() { override fun applyStyle(style: SessionEditorStyle) { this.style = style - val label = setFont(title, style.boldEditorFont) || setFont(sub, style.smallEditorFont) + val t = setFont(title, style.boldUiFont) + val s = setFont(sub, style.smallUiFont) + val label = t || s val body = texts.fold(false) { acc, item -> setFont(item.first, item.second) || acc } if (!label && !body) return refresh() @@ -137,6 +139,9 @@ class QuestionResultView(tool: Tool) : PartView() { fun bodyFonts(): List = texts.map { it.first.font } + fun titleFont(): Font = title.font + fun subFont(): Font = sub.font + override fun dumpLabel(): String = "QuestionResultView#$contentId(${labelText()})" companion object { @@ -268,7 +273,7 @@ class QuestionResultView(tool: Tool) : PartView() { } private fun setFont(area: JBTextArea, bold: Boolean): Boolean { - val font = if (bold) style.boldEditorFont else style.transcriptFont + val font = if (bold) style.boldUiFont else style.uiFont if (area.font == font) return false area.font = font return true diff --git a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/views/question/QuestionView.kt b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/views/question/QuestionView.kt index 2711604742f..2ccb0b74d12 100644 --- a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/views/question/QuestionView.kt +++ b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/views/question/QuestionView.kt @@ -98,7 +98,6 @@ class QuestionView( nav.add(fwd) topPanel.add(summary, BorderLayout.WEST) topPanel.add(nav, BorderLayout.EAST) - footer.add(dismiss, BorderLayout.WEST) footer.add(right, BorderLayout.EAST) card.setTopPanel(topPanel) @@ -176,9 +175,11 @@ class QuestionView( private fun syncFooter(q: Question) { right.removeAll() + right.add(dismiss) if (review(q)) { val back = dismissButton(KiloBundle.message("session.question.back")) { goBack() } val submit = applyButton(KiloBundle.message("session.question.submit")) { doReply() } + right.add(Box.createHorizontalStrut(UiStyle.Gap.sm())) right.add(back) right.add(Box.createHorizontalStrut(UiStyle.Gap.sm())) right.add(submit) @@ -200,6 +201,7 @@ class QuestionView( } } } + right.add(Box.createHorizontalStrut(UiStyle.Gap.sm())) right.add(button) } @@ -207,8 +209,10 @@ class QuestionView( val ready = selections.getOrNull(idx)?.isNotEmpty() == true back.isEnabled = idx > 0 fwd.isEnabled = idx < q.items.size && ready + val backLabel = KiloBundle.message("session.question.back") + val dismissLabel = KiloBundle.message("session.question.dismiss") for (node in right.components) { - if (node is SessionQuestionButton && node.text != KiloBundle.message("session.question.back")) { + if (node is SessionQuestionButton && node.text != backLabel && node.text != dismissLabel) { node.isEnabled = review(q) || ready } } @@ -436,7 +440,7 @@ class QuestionView( } private fun setFont(area: JBTextArea, bold: Boolean): Boolean { - val font = if (bold) style.boldEditorFont else style.transcriptFont + val font = if (bold) style.boldUiFont else style.uiFont if (area.font == font) return false area.font = font return true diff --git a/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/session/ui/SessionEditorStyleTest.kt b/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/session/ui/SessionEditorStyleTest.kt index b11df3d3320..9d4c32ce42f 100644 --- a/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/session/ui/SessionEditorStyleTest.kt +++ b/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/session/ui/SessionEditorStyleTest.kt @@ -50,4 +50,22 @@ class SessionEditorStyleTest : BasePlatformTestCase() { assertTrue(style.smallEditorFont.size < style.editorSize) assertEquals(style.editorSize, style.uiFont.size) } + + fun `test ui fonts use platform label family not editor family`() { + val style = SessionEditorStyle.create(family = "Courier New", size = 22) + + // uiFont / boldUiFont / smallUiFont must NOT use the editor font family + assertFalse("uiFont should not use editor font family", style.uiFont.name == "Courier New") + assertFalse("boldUiFont should not use editor font family", style.boldUiFont.name == "Courier New") + assertFalse("smallUiFont should not use editor font family", style.smallUiFont.name == "Courier New") + } + + fun `test ui fonts inherit editor size`() { + val style = SessionEditorStyle.create(family = "Courier New", size = 22) + + assertEquals("uiFont size should match editor size", 22, style.uiFont.size) + assertEquals("boldUiFont size should match editor size", 22, style.boldUiFont.size) + assertTrue("boldUiFont should be bold", style.boldUiFont.isBold) + assertTrue("smallUiFont should be smaller than editor size", style.smallUiFont.size < style.editorSize) + } } diff --git a/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/session/ui/shared/BaseSessionQuestionPanelTest.kt b/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/session/ui/shared/BaseSessionQuestionPanelTest.kt index c9b05745096..b6acff41f82 100644 --- a/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/session/ui/shared/BaseSessionQuestionPanelTest.kt +++ b/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/session/ui/shared/BaseSessionQuestionPanelTest.kt @@ -1,8 +1,12 @@ package ai.kilocode.client.session.ui.shared +import ai.kilocode.client.session.ui.style.SessionEditorStyle +import com.intellij.icons.AllIcons import com.intellij.openapi.application.ApplicationManager import com.intellij.testFramework.fixtures.BasePlatformTestCase +import com.intellij.ui.components.JBLabel import com.intellij.ui.components.JBTextArea +import java.awt.BorderLayout import java.awt.Container import javax.swing.JComponent import javax.swing.JLabel @@ -40,7 +44,7 @@ class BaseSessionQuestionPanelTest : BasePlatformTestCase() { val col = findCol(panel)!! val comps = col.components.toList() val topIdx = comps.indexOf(top) - val headerIdx = comps.indexOf(panel.headerText) + val headerIdx = comps.indexOf(panel.headerText.parent) assertTrue("top should appear before headerText", topIdx < headerIdx) } } @@ -169,7 +173,7 @@ class BaseSessionQuestionPanelTest : BasePlatformTestCase() { val col = findCol(panel)!! val comps = col.components.toList() val topIdx = comps.indexOf(top) - val headerIdx = comps.indexOf(panel.headerText) + val headerIdx = comps.indexOf(panel.headerText.parent) val descIdx = comps.indexOf(panel.descriptionText) val bodyIdx = comps.indexOf(body) val footerIdx = comps.indexOf(footer) @@ -195,7 +199,38 @@ class BaseSessionQuestionPanelTest : BasePlatformTestCase() { edt { val panel = BaseSessionQuestionPanel() val col = findCol(panel)!! - assertEquals("headerText + descriptionText only", 2, col.componentCount) + assertEquals("header row + descriptionText only", 2, col.componentCount) + } + } + + // ------ header left icon ------ + + fun `test setHeaderIcon adds icon to the left side of header row`() { + edt { + val panel = BaseSessionQuestionPanel() + panel.setHeaderIcon(AllIcons.General.Warning, "warning") + + val header = panel.headerText.parent as JPanel + val layout = header.layout as BorderLayout + val labels = findAll(header).filter { it.icon != null } + assertEquals("Expected one header icon", 1, labels.size) + assertSame(AllIcons.General.Warning, labels[0].icon) + assertEquals("warning", labels[0].toolTipText) + assertEquals(BorderLayout.WEST, layout.getConstraints(labels[0])) + assertEquals(BorderLayout.CENTER, layout.getConstraints(panel.headerText)) + } + } + + fun `test setHeaderIcon null hides header icon without removing header row`() { + edt { + val panel = BaseSessionQuestionPanel() + panel.setHeaderIcon(AllIcons.General.Warning) + panel.setHeaderIcon(null) + + val header = panel.headerText.parent as Container + val labels = findAll(header).filter { it.icon != null && it.isVisible } + assertTrue("Header icon should be hidden after setHeaderIcon(null)", labels.isEmpty()) + assertSame(header, panel.headerText.parent) } } @@ -243,6 +278,36 @@ class BaseSessionQuestionPanelTest : BasePlatformTestCase() { } } + // ------ applyStyle: UI fonts ------ + + fun `test applyStyle applies boldUiFont to header and uiFont to description`() { + edt { + val panel = BaseSessionQuestionPanel() + val style = SessionEditorStyle.create(family = "Courier New", size = 20) + panel.applyStyle(style) + + assertEquals("headerText should use boldUiFont", style.boldUiFont, panel.headerText.font) + assertEquals("descriptionText should use uiFont", style.uiFont, panel.descriptionText.font) + } + } + + fun `test applyStyle does not apply editor font family to header or description`() { + edt { + val panel = BaseSessionQuestionPanel() + val style = SessionEditorStyle.create(family = "Courier New", size = 20) + panel.applyStyle(style) + + assertFalse( + "headerText should not use editor font family", + panel.headerText.font.name == "Courier New", + ) + assertFalse( + "descriptionText should not use editor font family", + panel.descriptionText.font.name == "Courier New", + ) + } + } + // ------ helpers ------ private fun edt(block: () -> T): T { @@ -270,4 +335,15 @@ class BaseSessionQuestionPanelTest : BasePlatformTestCase() { } return null } + + private inline fun findAll(root: Container): List = findAllCls(root, T::class.java) + + private fun findAllCls(root: Container, cls: Class): List { + val result = mutableListOf() + if (cls.isInstance(root)) result.add(cls.cast(root)) + for (child in root.components) { + if (child is Container) result.addAll(findAllCls(child, cls)) + } + return result + } } diff --git a/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/session/views/LoginRequiredViewTest.kt b/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/session/views/LoginRequiredViewTest.kt index 6b2bd18d909..b7972df3a08 100644 --- a/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/session/views/LoginRequiredViewTest.kt +++ b/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/session/views/LoginRequiredViewTest.kt @@ -1,6 +1,7 @@ package ai.kilocode.client.session.views import ai.kilocode.client.session.ui.shared.SessionQuestionButton +import ai.kilocode.client.session.ui.style.SessionEditorStyle import ai.kilocode.client.session.ui.style.SessionUiStyle import com.intellij.ide.ui.laf.darcula.ui.DarculaButtonUI import com.intellij.openapi.application.ApplicationManager @@ -105,6 +106,18 @@ class LoginRequiredViewTest : BasePlatformTestCase() { } } + fun `test login action buttons share right-aligned footer group`() { + edt { + val view = LoginRequiredView(openProfile = {}, dismiss = {}) + view.show("Sign in required.") + + val dismiss = view.dismissButton + val open = view.openProfileButton + assertSame("Dismiss and open profile should be in the same right-aligned group", dismiss.parent, open.parent) + assertTrue("Dismiss should appear before open profile", dismiss.parent.components.indexOf(dismiss) < open.parent.components.indexOf(open)) + } + } + // ------ callbacks ------ fun `test open profile button click invokes openProfile callback`() { @@ -161,6 +174,42 @@ class LoginRequiredViewTest : BasePlatformTestCase() { } } + // ------ fonts: UI family, editor size ------ + + fun `test header uses boldUiFont not editor font family`() { + edt { + val view = LoginRequiredView(openProfile = {}, dismiss = {}) + view.show("Sign in required.") + val style = SessionEditorStyle.create(family = "Courier New", size = 20) + view.applyStyle(style) + + val title = findAll(view).firstOrNull { it.font.isBold } + assertNotNull("Bold title text area should be present", title) + assertFalse( + "Title font should not use editor font family", + title!!.font.name == "Courier New", + ) + assertEquals("Title font size should match editor size", 20, title.font.size) + } + } + + fun `test description uses uiFont not editor font family`() { + edt { + val view = LoginRequiredView(openProfile = {}, dismiss = {}) + view.show("Sign in required.") + val style = SessionEditorStyle.create(family = "Courier New", size = 20) + view.applyStyle(style) + + val desc = findAll(view).firstOrNull { it.text == "Sign in required." } + assertNotNull("Description text area should be present", desc) + assertFalse( + "Description font should not use editor font family", + desc!!.font.name == "Courier New", + ) + assertEquals("Description font size should match editor size", 20, desc.font.size) + } + } + // ------ helpers ------ private fun edt(block: () -> T): T { diff --git a/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/session/views/PermissionViewTest.kt b/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/session/views/PermissionViewTest.kt index 24693425a03..d7153b0c90f 100644 --- a/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/session/views/PermissionViewTest.kt +++ b/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/session/views/PermissionViewTest.kt @@ -5,11 +5,14 @@ import ai.kilocode.client.session.model.PermissionFileDiff import ai.kilocode.client.session.model.PermissionMeta import ai.kilocode.client.session.model.PermissionRequestState import ai.kilocode.client.session.ui.shared.BaseSessionQuestionPanel +import ai.kilocode.client.session.ui.style.SessionEditorStyle import ai.kilocode.client.session.ui.style.SessionUiStyle import ai.kilocode.rpc.dto.PermissionReplyDto +import com.intellij.icons.AllIcons import com.intellij.ide.ui.laf.darcula.ui.DarculaButtonUI import com.intellij.testFramework.fixtures.BasePlatformTestCase import com.intellij.ui.components.JBHtmlPane +import com.intellij.ui.components.JBLabel import com.intellij.ui.components.JBScrollPane import java.awt.Container import javax.swing.AbstractButton @@ -262,6 +265,16 @@ class PermissionViewTest : BasePlatformTestCase() { assertTrue("Expected a BaseSessionQuestionPanel after show", panels.isNotEmpty()) } + fun `test permission icon is rendered in header`() { + view.show(permission()) + + val labels = findAll(view) + assertTrue( + "Expected permission warning icon in header", + labels.any { it.icon == AllIcons.General.Warning }, + ) + } + // ------ new: shared button types ------ fun `test run button is SessionQuestionButton with primary true`() { @@ -355,6 +368,47 @@ class PermissionViewTest : BasePlatformTestCase() { assertEquals(SessionUiStyle.View.headerHover(), scroll.viewport.background) } + // ------ fonts: header UI family, command code block editor family ------ + + fun `test permission header uses boldUiFont not editor font family`() { + view.show( + Permission( + id = "perm_font", + sessionId = "ses", + name = "bash", + patterns = emptyList(), + always = emptyList(), + meta = PermissionMeta(command = "ls"), + ) + ) + val style = SessionEditorStyle.create(family = "Courier New", size = 18) + view.applyStyle(style) + + val header = view.headerFontForTest() + assertFalse("Permission header should not use editor font family", header.name == "Courier New") + assertTrue("Permission header should be bold", header.isBold) + assertEquals("Permission header size should match editor size", 18, header.size) + } + + fun `test command code block retains editor font family`() { + view.show( + Permission( + id = "perm_codefont", + sessionId = "ses", + name = "bash", + patterns = emptyList(), + always = emptyList(), + meta = PermissionMeta(command = "git log"), + ) + ) + val style = SessionEditorStyle.create(family = "Courier New", size = 18) + view.applyStyle(style) + + val md = view.firstCmdViewForTest() + assertNotNull("Should have at least one command MdView", md) + assertEquals("Code block codeFont should use editor family", "Courier New", md!!.codeFont) + } + private fun permission() = Permission( id = "perm1", sessionId = "ses_test", diff --git a/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/session/views/QuestionResultViewTest.kt b/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/session/views/QuestionResultViewTest.kt index 9038cbffb3e..346ffb97d8d 100644 --- a/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/session/views/QuestionResultViewTest.kt +++ b/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/session/views/QuestionResultViewTest.kt @@ -166,7 +166,7 @@ class QuestionResultViewTest : BasePlatformTestCase() { // ------ applyStyle ------ - fun `test applyStyle updates fonts`() { + fun `test applyStyle updates body fonts to UI font family`() { val tool = completedTool( input = mapOf("questions" to """[{"question":"Q1"}]"""), metadata = mapOf("answers" to """[["A1"]]"""), @@ -177,8 +177,25 @@ class QuestionResultViewTest : BasePlatformTestCase() { view.applyStyle(style) view.toggle() - assertTrue(view.bodyFonts().contains(style.transcriptFont)) - assertTrue(view.bodyFonts().contains(style.boldEditorFont)) + assertTrue(view.bodyFonts().contains(style.uiFont)) + assertTrue(view.bodyFonts().contains(style.boldUiFont)) + assertFalse("Body should not use editor transcript font", view.bodyFonts().any { it.name == "Courier New" }) + } + + fun `test applyStyle updates header label fonts to UI font family`() { + val tool = completedTool( + input = mapOf("questions" to """[{"question":"Q1"}]"""), + metadata = mapOf("answers" to """[["A1"]]"""), + ) + val view = QuestionResultView(tool) + val style = SessionEditorStyle.create(family = "Courier New", size = 22) + + view.applyStyle(style) + + assertEquals("Title should use boldUiFont", style.boldUiFont, view.titleFont()) + assertEquals("Subtitle should use smallUiFont", style.smallUiFont, view.subFont()) + assertFalse("Title should not use editor font family", view.titleFont().name == "Courier New") + assertFalse("Subtitle should not use editor font family", view.subFont().name == "Courier New") } // ------ update ------ diff --git a/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/session/views/QuestionViewTest.kt b/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/session/views/QuestionViewTest.kt index dff95461e65..6932aad49e2 100644 --- a/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/session/views/QuestionViewTest.kt +++ b/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/session/views/QuestionViewTest.kt @@ -87,6 +87,31 @@ class QuestionViewTest : BasePlatformTestCase() { assertTrue(replies.isEmpty()) } + fun `test question action buttons share right-aligned footer group`() { + view.show(singleSelectQuestion("req_actions")) + + val dismiss = button(view, "Dismiss") + val submit = button(view, "Submit") + assertSame("Dismiss and Submit should be in the same right-aligned group", dismiss.parent, submit.parent) + assertTrue("Dismiss should appear before Submit", dismiss.parent.components.indexOf(dismiss) < submit.parent.components.indexOf(submit)) + } + + fun `test review action buttons share right-aligned footer group`() { + view.show(twoItemQuestion("req_review_actions")) + option(view, "Minimal").doClick() + button(view, "Next").doClick() + option(view, "Unit").doClick() + button(view, "Review").doClick() + + val dismiss = button(view, "Dismiss") + val back = button(view, "Back") + val submit = button(view, "Submit") + assertSame("Dismiss and Back should be in the same right-aligned group", dismiss.parent, back.parent) + assertSame("Back and Submit should be in the same right-aligned group", back.parent, submit.parent) + assertTrue("Dismiss should appear before Back", dismiss.parent.components.indexOf(dismiss) < back.parent.components.indexOf(back)) + assertTrue("Back should appear before Submit", back.parent.components.indexOf(back) < submit.parent.components.indexOf(submit)) + } + // ------ radio options ------ fun `test single question renders radio options`() { @@ -159,19 +184,19 @@ class QuestionViewTest : BasePlatformTestCase() { assertEquals("description should align in the text renderer", label.parent, desc.parent) val style = SessionEditorStyle.current() - assertEquals("option label should use bold editor font", style.boldEditorFont, label.font) - assertEquals("description should use transcript font", style.transcriptFont, desc.font) + assertEquals("option label should use boldUiFont", style.boldUiFont, label.font) + assertEquals("description should use uiFont", style.uiFont, desc.font) } - fun `test question title and hint use editor fonts`() { + fun `test question title and hint use UI-family editor-sized fonts`() { view.show(singleSelectQuestion("q_fonts")) val style = SessionEditorStyle.current() val title = text(view, "Choose approach") val hint = text(view, "Select one answer") - assertEquals(style.boldEditorFont, title.font) - assertEquals(style.transcriptFont, hint.font) + assertEquals(style.boldUiFont, title.font) + assertEquals(style.uiFont, hint.font) } // ------ multi-question navigation ------