packages/kilo-jetbrains/.kilo/skills/jetbrains-ui-style/SKILL.md
Use this skill whenever creating, modifying, or reviewing UI code in packages/kilo-jetbrains.
This skill covers Kotlin/Swing UI code for the Kilo JetBrains plugin, including dialogs, settings pages, tool windows, forms, panels, component layout, sizing, spacing, colors, borders, icons, lists, trees, popups, notifications, and IntelliJ Platform UI components.
This plugin is a split-mode JetBrains plugin. UI code belongs in frontend unless there is a specific split-mode reason to place it elsewhere.
When looking for IntelliJ Platform API usage, implementation examples, extension points, services, actions, inspections, PSI/VFS/editor behavior, or plugin patterns, prefer real IntelliJ source code over Gradle caches, downloaded jars, generated parser artifacts, or decompiled classes.
Use this priority order:
$INTELLIJ_REPO is set and points to a readable IntelliJ Community checkout.
$INTELLIJ_REPO is set, search source files under that directory first.platform/, plugins/, java/, xml/, json/, jvm/, and related modules.$INTELLIJ_REPO is unset, empty, unreadable, or does not appear to contain an IntelliJ source checkout, tell the user to set it up.
Set INTELLIJ_REPO to the path of a local intellij-community checkout, for example: export INTELLIJ_REPO=/path/to/intellij-communityAvoid starting searches in:
~/.gradle/caches.gradle/isOpaque = false unless the component default differs or there is a documented rendering reason.JBUI utilities.UIUtil, JBUI.CurrentTheme, NamedColorUtil, JBColor.namedColor, or JBColor.lazy.JBFont, RelativeFont, or existing component fonts for typography.*.properties files.| Need | API |
|---|---|
| Dialogs, settings pages, forms, any layout with components | Preferred: Kotlin UI DSL v2 (com.intellij.ui.dsl.builder) |
| Tool window panels, action-driven UI, custom components | Standard Swing with IntelliJ Platform component replacements |
| Menus and toolbars | Action System |
Avoid explicit defaults and manual layout when the DSL can express the UI:
// Avoid
val panel = JPanel(BorderLayout()).apply {
isOpaque = false
preferredSize = Dimension(420, 260)
border = EmptyBorder(12, 12, 12, 12)
}
Prefer DSL and platform spacing:
panel {
row(KiloBundle.message("settings.name")) {
textField()
.align(AlignX.FILL)
.resizableColumn()
}
}
Avoid explicit default properties:
// Avoid unless there is a documented rendering reason
component.isOpaque = false
Prefer omitting the assignment and relying on the component/platform default.
Avoid raw fixed sizes:
// Avoid
component.preferredSize = Dimension(300, 40)
Prefer semantic sizing:
textField()
.columns(COLUMNS_MEDIUM)
.align(AlignX.FILL)
If a fixed size is genuinely required, use JBUI.size(...) or JBUI.scale(...) and keep the reason obvious from context.
Do not use Kotlin Compose or intellij.platform.compose in this plugin. The JetBrains modular template uses Compose for its demo tool window, but Kilo should use standard Swing with IntelliJ Platform components only. Keep all plugin UI in the existing Swing-based stack.
Do not use JCEF (JBCefBrowser) in this plugin. JCEF does not work in JetBrains remote development (split mode): the frontend process runs on the client machine but JCEF requires a display on the host, making it effectively unusable for remote users. Use standard Swing with IntelliJ Platform components for all UI.
Use Kotlin UI DSL v2 as the default way to build UI for dialogs, settings pages, forms, and any layout composed of standard components. It produces correct spacing, label alignment, HiDPI scaling, and accessibility automatically. Only fall back to manual Swing layout when you need a fully custom component, such as a canvas, rich list renderer, transcript surface, or tool-window chrome that the DSL cannot express.
The top-level builder is panel { } and returns DialogPanel. Structure is panel -> row -> cells. Cell factory methods such as textField(), checkBox(), and label() add components. The DSL lives in com.intellij.ui.dsl.builder.
To explore DSL capabilities interactively: Tools -> Internal Actions -> UI -> Kotlin UI DSL -> UI DSL Showcase. This requires internal mode: -Didea.is.internal=true.
Rows occupy full width. The last cell in a row takes remaining space. Rows have a layout property.
panel {
row("Row1 label:") {
textField()
label("Some text")
}
row("Row2:") {
label("This text is aligned with previous row")
}
}
Every row uses one of three layouts. Default is LABEL_ALIGNED when a label is provided for the row, INDEPENDENT otherwise.
| Layout | Behavior |
|---|---|
LABEL_ALIGNED | Label column and content columns, aligned across rows |
INDEPENDENT | All cells are independent, no cross-row alignment |
PARENT_GRID | Cells align with the parent grid columns across rows |
panel {
row("PARENT_GRID:") {
label("Col 1")
label("Col 2")
}.layout(RowLayout.PARENT_GRID)
row("PARENT_GRID:") {
textField()
textField()
}.layout(RowLayout.PARENT_GRID)
row("LABEL_ALIGNED default with label:") {
textField()
}
row {
label("INDEPENDENT default without label:")
textField()
}
}
All cell factory methods available inside row { }:
| Method | Description |
|---|---|
checkBox("text") | Checkbox |
threeStateCheckBox("text") | Three-state checkbox |
radioButton("text", value) | Radio button, must be inside buttonsGroup {} |
button("text") {} | Push button |
actionButton(action) | Icon button bound to an AnAction |
actionsButton(action1, action2, ...) | Dropdown actions button |
segmentedButton(items) { text = it } | Segmented control |
tabbedPaneHeader(items) | Tab header strip |
label("text") | Static label |
text("html") | Rich text with links, icons, line-width control |
link("text") {} | Focusable clickable link |
browserLink("text", "url") | Opens URL in browser |
dropDownLink("default", listOf(...)) | Dropdown link selector |
icon(AllIcons.*) | Icon display |
contextHelp("description", "title") | Help icon with popup |
textField() | Text input |
passwordField() | Password input |
textFieldWithBrowseButton() | Text field and browse dialog |
expandableTextField() | Expandable multi-line text field |
extendableTextField() | Text field with extension icons |
intTextField(range) | Integer input with validation |
spinner(intRange) / spinner(doubleRange, step) | Numeric spinner |
slider(min, max, minorTick, majorTick) | Slider, use .labelTable() for tick labels |
textArea() | Multi-line text, use .rows(n) and .align(AlignX.FILL) |
comboBox(items) | Combo box / dropdown |
comment("text") | Gray comment text, standalone |
cell(component) | Wrap any arbitrary Swing component |
scrollCell(component) | Wrap component in a scroll pane |
cell() | Empty placeholder cell for grid alignment |
Key component examples:
panel {
var mode = "auto"
buttonsGroup {
row("Mode:") {
radioButton("Automatic", "auto")
radioButton("Manual", "manual")
}
}.bind({ mode }, { mode = it })
row("Slider:") {
slider(0, 10, 1, 5)
.labelTable(mapOf(
0 to JBLabel("0"),
5 to JBLabel("5"),
10 to JBLabel("10"),
))
}
row {
label("Text area:")
.align(AlignY.TOP)
.gap(RightGap.SMALL)
textArea()
.rows(5)
.align(AlignX.FILL)
}.layout(RowLayout.PARENT_GRID)
}
Labels for modifiable components must be connected via one of two methods. This ensures correct spacing, mnemonic support, and accessibility.
row("&Label:") { textField() }, mnemonic via &, label in left columntextField().label("&Label:", LabelPosition.TOP), label attached to cell, optionally on toppanel {
row("&Row label:") {
textField()
textField()
.label("Cell label at &left:")
}
row {
textField()
.label("Cell label at &top:", LabelPosition.TOP)
}
}
When a row contains a checkBox or radioButton, the DSL automatically increases space after the row label per IntelliJ UI Guidelines.
Three types of comments, each with different placement and semantics:
| Type | Method | Placement |
|---|---|---|
| Cell comment, bottom | cell.comment("text") | Below the cell |
| Cell comment, right | cell.commentRight("text") | Right of the cell |
| Cell context help | cell.contextHelp("text", "title") | Help icon with popup |
| Row comment | row.rowComment("text") | Below the entire row |
| Arbitrary comment | comment("text") | Standalone gray text |
panel {
row {
textField()
.comment("Bottom comment")
textField()
.commentRight("Right comment")
textField()
.contextHelp("Help popup text")
}
row("Label:") {
textField()
}.rowComment("This comment sits below the whole row")
row {
comment("Standalone comment, supports <a href='link'>links</a> and <icon src='AllIcons.General.Information'> icons")
}
}
Comments support HTML with clickable links, bundled icons via <icon src='...'>, and line width control via maxLineLength. Use MAX_LINE_LENGTH_NO_WRAP to prevent wrapping.
| Method | Grid | Description |
|---|---|---|
panel {} | Own grid | Sub-panel occupying full width |
rowsRange {} | Parent grid | Grouped rows sharing parent grid, useful with enabledIf |
group("Title") {} | Own grid | Titled section with vertical spacing before/after |
groupRowsRange("Title") {} | Parent grid | Titled section sharing parent grid alignment |
collapsibleGroup("Title") {} | Own grid | Expandable section, Tab-focusable, supports mnemonics |
buttonsGroup("Title") {} | None | Groups radioButton or checkBox under a title |
separator() | None | Horizontal separator line |
Row panel {} | Own grid | Sub-panel inside a cell |
panel {
group("Settings") {
row("Name:") { textField() }
row("Path:") { textFieldWithBrowseButton() }
}
collapsibleGroup("Advanced") {
row("Timeout:") { intTextField(0..1000) }
}
var enabled = true
buttonsGroup("Mode:") {
row { radioButton("Automatic", true) }
row { radioButton("Manual", false) }
}.bind({ enabled }, { enabled = it })
separator()
row {
label("Nested panels:")
panel {
row("Sub row 1:") { textField() }
row("Sub row 2:") { textField() }
}
}
}
cell.gap(RightGap.SMALL) between a label-like checkbox and its related field. Medium gap is the default between cells.twoColumnsRow({}, {}) or gap(RightGap.COLUMNS) with .layout(RowLayout.PARENT_GRID).indent {} for indented sub-content..topGap(TopGap.MEDIUM) / .bottomGap(BottomGap.MEDIUM) on rows to separate unrelated groups. Attach gaps to the related row so hiding rows does not break layout.Common Kotlin UI DSL spacing values from IntelliJSpacingConfiguration:
| Constant | Unscaled px | Usage |
|---|---|---|
RightGap.SMALL | 6 | Related inline components, such as field to button |
| Default row/cell gap | 16 | Automatic gap between components in a row |
RightGap.COLUMNS | 60 | Logical column separation |
TopGap.SMALL / BottomGap.SMALL | 8 | Minor section separation |
TopGap.MEDIUM / BottomGap.MEDIUM | 20 | Major section or group separation |
verticalComponentGap | 6 | Automatic gap between rows |
These are semantic IntelliJ spacing constants. They handle DPI scaling and the IntelliJ spacing model, but they are not theme keys and are not overridden by theme JSON.
panel {
group("Horizontal Gaps") {
row {
val cb = checkBox("Use mail:")
.gap(RightGap.SMALL)
textField()
.enabledIf(cb.selected)
}
row("Width:") {
textField()
.gap(RightGap.SMALL)
label("pixels")
}
}
group("Indent") {
row { label("Not indented") }
indent {
row { label("Indented row") }
}
}
group("Two Columns") {
twoColumnsRow({
checkBox("First column")
}, {
checkBox("Second column")
})
}
group("Vertical Gaps") {
row { checkBox("Option 1") }
row { checkBox("Option 2") }
row { checkBox("Unrelated option") }
.topGap(TopGap.MEDIUM)
}
}
Bind enabled/visible state to a checkbox or other observable. Works on rows, indent {} blocks, rowsRange {}, and individual cells.
panel {
group("Enabled") {
lateinit var cb: Cell<JBCheckBox>
row { cb = checkBox("Enable options") }
indent {
row { checkBox("Option 1") }
row { checkBox("Option 2") }
}.enabledIf(cb.selected)
}
group("Visible") {
lateinit var cb: Cell<JBCheckBox>
row { cb = checkBox("Show options") }
indent {
row { checkBox("Option 1") }
row { checkBox("Option 2") }
}.visibleIf(cb.selected)
}
}
Bind component values to model properties. Values are applied on DialogPanel.apply(), checked with .isModified(), and reverted with .reset().
| Method | Component |
|---|---|
bindSelected(model::prop) | checkBox |
bindText(model::prop) | textField |
bindIntText(model::prop) | intTextField |
bindItem(model::prop.toNullableProperty()) | comboBox |
bindValue(model::prop) | slider |
bindIntValue(model::prop) | spinner |
buttonsGroup {}.bind(model::prop) | radio group |
enum class Theme { LIGHT, DARK }
data class Settings(
var name: String = "",
var count: Int = 0,
var enabled: Boolean = false,
var theme: Theme = Theme.LIGHT,
)
val model = Settings()
val panel = panel {
row("Name:") {
textField().bindText(model::name)
}
row("Count:") {
intTextField(0..100).bindIntText(model::count)
}
row {
checkBox("Enabled").bindSelected(model::enabled)
}
buttonsGroup("Theme:") {
row { radioButton("Light", Theme.LIGHT) }
row { radioButton("Dark", Theme.DARK) }
}.bind(model::theme)
}
panel.isModified()
panel.apply()
panel.reset()
Attach input validation rules to cells. Rules run continuously and display inline error/warning indicators.
panel {
row("Username:") {
textField()
.columns(COLUMNS_MEDIUM)
.cellValidation {
addInputRule("Must not be empty") {
it.text.isBlank()
}
}
}
row("Port:") {
textField()
.columns(COLUMNS_MEDIUM)
.cellValidation {
addInputRule("Contains non-numeric characters", level = Level.WARNING) {
it.text.contains(Regex("[^0-9]"))
}
}
}
}
Activate validators by calling dialogPanel.registerValidators(disposable) after creating the panel.
| Pattern | Usage |
|---|---|
.bold() | Bold text on any cell |
.columns(COLUMNS_MEDIUM) | Set preferred width of textField / comboBox / textArea |
.text("initial") | Set initial text on text components |
.resizableColumn() | Column fills remaining horizontal space |
cell() | Empty placeholder cell for grid alignment |
.widthGroup("name") | Equalize widths across rows, cannot combine with AlignX.FILL |
.align(AlignX.FILL) | Stretch component to fill available width |
.align(AlignY.TOP) | Top-align component in its cell |
.applyToComponent { } | Direct access to the underlying Swing component |
.selected(true) | Default-select a radioButton when no bound value matches |
.gap(RightGap.COLUMNS) | Column-level gap for multi-column layouts |
panel {
row { label("Title").bold() }
row("Name:") {
textField()
.columns(COLUMNS_MEDIUM)
.resizableColumn()
.align(AlignX.FILL)
}
row("") {
textField()
}.rowComment("""Use row("") for an empty label column that aligns with labeled rows""")
row {
text("Comment-colored text")
.applyToComponent { foreground = JBUI.CurrentTheme.ContextHelp.FOREGROUND }
}
}
Use manual Swing only when Kotlin UI DSL cannot express the UI cleanly.
Preferred manual Swing rules:
JBUI.Borders.empty(...), JBUI.insets(...), JBUI.size(...), and JBUI.scale(...) instead of raw AWT/Swing sizing primitives.JBUI.scale(...) for pixel values.Swing is retained-mode UI, not React-style declarative rendering. For dynamic Swing surfaces such as session cards, transcript parts, hover rows, and collapsible panels, build a stable component tree once and then mutate existing components in response to model or interaction changes.
state -> render() loop for Swing components.render() methods that remove/recreate headers, text areas, markdown views, controls, or scroll panes after every click, hover, or model update.update(model) method that applies model changes directly to existing UI components.update(model), compare before assigning when practical: label text, icons, foregrounds, fonts, body text, visibility, cursor, and containment.scroll.parent === root, rather than maintaining an open boolean.hover boolean.JBTextArea, JBScrollPane, markdown panes, and HTML panes on first expansion or first direct access.SessionStyle when they are first created.revalidate()/repaint() only when they changed preferred size, visibility, containment, or paint output.render() in Swing views tend to invite full tree rebuilds. Prefer names that describe the mutation, such as syncBody(), syncArrow(), syncHtml(), or applyModel().renderer.render(...); the problem is UI-level rerendering that rebuilds Swing component trees.Avoid this React-style pattern:
private var open = false
fun toggle() {
open = !open
render()
}
private fun render() {
root.removeAll()
header.removeAll()
header.add(title)
if (open) root.add(JBScrollPane(JBTextArea(text)), BorderLayout.CENTER)
revalidate()
repaint()
}
Prefer mutating stable Swing components and deriving state from the UI:
fun isExpanded(): Boolean = scroll?.parent === root
fun toggle() {
if (!canExpand()) return
val changed = if (isExpanded()) collapse() else expand()
if (!changed) return
syncArrow()
revalidate()
repaint()
}
private fun expand(): Boolean {
if (isExpanded()) return false
root.add(scroll(), BorderLayout.CENTER)
return true
}
private fun collapse(): Boolean {
val pane = scroll ?: return false
if (pane.parent !== root) return false
root.remove(pane)
return true
}
private fun scroll(): JBScrollPane {
scroll?.let { return it }
val pane = JBScrollPane(body())
scroll = pane
return pane
}
For tests around dynamic Swing components, assert retained component behavior instead of only final text:
isExpanded() agrees with actual containment.update(model) changes existing labels/body text without duplicating components.Use semantic spacing APIs before inventing numbers. Do not copy fallback values from IntelliJ source into generated UI code; those values are defaults behind theme keys and may change by UI theme, New UI, OS, or IDE version.
| Need | Preferred source |
|---|---|
| Dialogs, forms, settings | Kotlin UI DSL row/group/indent/gap APIs |
| Component gaps in Kotlin UI DSL | RightGap.SMALL, RightGap.COLUMNS, TopGap.MEDIUM, BottomGap.MEDIUM, .customize(UnscaledGaps(...)) as a last resort |
| Manual Swing empty padding | JBUI.Borders.empty(...) |
| Manual Swing insets | JBUI.insets(...), JBUI.emptyInsets(), JBUI.insetsTop(...), JBUI.insetsLeft(...), JBUI.insetsBottom(...), JBUI.insetsRight(...) |
| Manual Swing dimensions | JBUI.size(...), JBDimension, JBUI.scale(...) |
| Side separators | JBUI.Borders.customLineTop(...), customLineBottom(...), customLineLeft(...), customLineRight(...) |
| Composed borders | JBUI.Borders.compound(...), JBUI.Borders.merge(...) |
Simple BorderLayout panels | JBUI.Panels.simplePanel(...), BorderLayoutPanel |
| Simple vertical custom Swing groups | VerticalLayout |
| Fluent platform panels | JBPanel.withBorder(...), .andTransparent(), .andOpaque(), .withBackground(...) |
For themeable custom layouts, keep the JBValue.UIInteger object and call .get() during layout and size calculation. Do not cache the resolved Int, because that freezes the value from the current Look and Feel and scale context.
private val gap = JBValue.UIInteger("KiloComponent.gap", 16)
override fun doLayout() {
val value = gap.get()
// layout using value
}
override fun getPreferredSize(): Dimension {
val value = gap.get()
// calculate using value
}
Avoid resolving the value once in a constructor or property initializer:
private val gap = JBValue.UIInteger("KiloComponent.gap", 16).get()
When raw AWT layout constructors are unavoidable, legacy Swing defaults exist but must still be scaled before use:
| Constant | Value |
|---|---|
UIUtil.DEFAULT_HGAP | 10 |
UIUtil.DEFAULT_VGAP | 4 |
UIUtil.LARGE_VGAP | 12 |
val panel = JPanel(BorderLayout(JBUI.scale(UIUtil.DEFAULT_HGAP), 0))
Use JBUI.CurrentTheme for context-specific spacing and dimensions:
| UI area | Preferred source |
|---|---|
| Action list rows | JBUI.CurrentTheme.ActionsList.cellPadding() |
| Action icon/text gaps | JBUI.CurrentTheme.ActionsList.elementIconGap() |
| Action mnemonic gaps | JBUI.CurrentTheme.ActionsList.mnemonicIconGap() / mnemonicInsets() |
| Popup selection interior | JBUI.CurrentTheme.Popup.Selection.innerInsets() |
| Popup separators | JBUI.CurrentTheme.Popup.separatorInsets() / separatorLabelInsets() |
| Popup header/search field | JBUI.CurrentTheme.Popup.headerInsets(), searchFieldBorderInsets(), searchFieldInputInsets() |
| Complex popup headers | JBUI.CurrentTheme.ComplexPopup.headerInsets() |
| Complex popup text fields | JBUI.CurrentTheme.ComplexPopup.textFieldBorderInsets() / textFieldInputInsets() |
| Search Everywhere / big popup header | JBUI.CurrentTheme.BigPopup.headerBorder() / headerToolbarInsets() / tabInsets() |
| Popup advertiser/footer | JBUI.CurrentTheme.Advertiser.border() |
| Toolbar buttons | JBUI.CurrentTheme.Toolbar.toolbarButtonInsets() / mainToolbarButtonInsets() |
| Toolbar containers | JBUI.CurrentTheme.Toolbar.horizontalToolbarInsets() / verticalToolbarInsets() |
| Tool-window headers | JBUI.CurrentTheme.ToolWindow.headerLabelLeftRightInsets() / headerToolbarLeftRightInsets() / headerTabLeftRightInsets() |
| Lists | JBUI.CurrentTheme.List.rowHeight() |
| Trees | JBUI.CurrentTheme.Tree.rowHeight() |
| VCS log rows | JBUI.CurrentTheme.VersionControl.Log.rowHeight() / verticalPadding() |
| Help tooltips | JBUI.CurrentTheme.HelpTooltip.defaultTextBorderInsets() / smallTextBorderInsets() |
| Navigation bar items | JBUI.CurrentTheme.NavBar.itemInsets() |
When using Kotlin UI DSL, prefer DSL semantics over manual padding:
panel {
group(KiloBundle.message("settings.section")) {
row(KiloBundle.message("settings.name")) {
textField()
.align(AlignX.FILL)
.resizableColumn()
}
indent {
row { checkBox(KiloBundle.message("settings.enabled")) }
}
}
}
When manual Swing is necessary, get padding from platform utilities:
val panel = JBUI.Panels.simplePanel(content)
.withBorder(JBUI.Borders.empty(JBUI.CurrentTheme.Popup.headerInsets()))
For row heights, prefer platform row height APIs over fixed heights:
component.preferredSize = JBDimension(0, JBUI.CurrentTheme.Tree.rowHeight())
Use existing platform utility classes instead of hand-rolled labels, renderers, borders, colors, fonts, and validation behavior.
| Need | Preferred API |
|---|---|
| Concise border layout | BorderLayoutPanel, JBUI.Panels.simplePanel(...) |
| Platform panel helpers | JBPanel.withBorder(...), .andTransparent(), .andOpaque() |
| Platform label behavior | JBLabel |
| Context help | ContextHelpLabel |
| Links | HyperlinkLabel, LinkLabel, Kotlin UI DSL link(...) / browserLink(...) |
| Error/validation label in legacy UI | ErrorLabel only when an inline validation component is required and DSL validation is not available |
| High-performance rich fragments | SimpleColoredComponent |
| List renderers | ColoredListCellRenderer, Kotlin listCellRenderer / textListCellRenderer / LcrRow |
| Tree renderers | ColoredTreeCellRenderer |
| Renderer text styles | SimpleTextAttributes |
| Editable list toolbar | ToolbarDecorator |
| Platform list | JBList |
| Platform tree | Tree |
For renderer text, prefer SimpleTextAttributes over direct color/font mutation:
append(text, SimpleTextAttributes.ERROR_ATTRIBUTES)
append(detail, SimpleTextAttributes.GRAYED_ATTRIBUTES)
append(link, SimpleTextAttributes.LINK_ATTRIBUTES)
com.intellij.toolWindow extension point.ToolWindowFactory.createToolWindowContent(), called lazily on first click.SimpleToolWindowPanel(vertical = true) as a convenient base for toolbar and content layout.ToolWindow.contentManager: create content with ContentFactory.getInstance().createContent(component, title, isLockable), then contentManager.addContent().ToolWindowFactory.isApplicableAsync(project).ToolWindowManager.invokeLater() instead of Application.invokeLater() for tool-window-related EDT tasks.DialogWrapper.init() from the constructor.createCenterPanel() to return UI content.getPreferredFocusedComponent() for initial focus.getDimensionServiceKey() for size persistence when useful.showAndGet() for modal boolean result, or show() and then getExitCode().initValidation() in the constructor and override doValidate() to return null if valid or ValidationInfo(message, component) if invalid.Always use IntelliJ platform components instead of raw Swing where an equivalent exists.
| Instead of | Use | Package |
|---|---|---|
JLabel | JBLabel | com.intellij.ui.components |
JTextField | JBTextField | com.intellij.ui.components |
JTextArea | JBTextArea | com.intellij.ui.components |
JList | JBList | com.intellij.ui.components |
JScrollPane | JBScrollPane | com.intellij.ui.components |
JTable | JBTable | com.intellij.ui.table |
JTree | Tree | com.intellij.ui.treeStructure |
JSplitPane | JBSplitter | com.intellij.ui |
JTabbedPane | JBTabs | com.intellij.ui.tabs |
JCheckBox | JBCheckBox | com.intellij.ui.components |
| Raw runtime colors | UIUtil, JBUI.CurrentTheme, NamedColorUtil, JBColor.namedColor, JBColor.lazy | com.intellij.util.ui, com.intellij.ui |
EmptyBorder | JBUI.Borders.empty() | com.intellij.util.ui |
| Hardcoded pixel sizes | JBUI.scale(px) | com.intellij.util.ui |
Inspection Plugin DevKit | Code | Undesirable class usage highlights raw Swing usage where a platform replacement exists.
| Need | Component |
|---|---|
| Rich HTML with modern CSS, icons, shortcuts | JBHtmlPane (com.intellij.ui.components.JBHtmlPane) |
| Simple multi-line label with HTML | JBLabel + XmlStringUtil.wrapInHtml() |
| Scrollable / wrapping HTML panel | SwingHelper.createHtmlViewer() |
| High-performance colored text fragments in trees/lists/tables | SimpleColoredComponent |
| Plain-text newline splitting | MultiLineLabel, legacy, do not use in new code |
HtmlChunk / HtmlBuilder (com.intellij.openapi.util.text.HtmlChunk). Avoid raw HTML string concatenation because it risks injection and breaks localization.XmlStringUtil.wrapInHtml(content), XmlStringUtil.wrapInHtmlLines(lines...), and XmlStringUtil.escapeString(text).JBLabel.setCopyable(true), which switches internally to JEditorPane while preserving label appearance. Use setAllowAutoWrapping(true) for auto-wrap.JEditorPane manually, always use HTMLEditorKitBuilder instead of constructing HTMLEditorKit directly: editorPane.setEditorKit(HTMLEditorKitBuilder.simple()) or .withWordWrapViewFactory().build().SwingTextTrimmer. Do not manually truncate strings.*.properties files. HTML markup in values is acceptable.Do not hardcode runtime colors. IntelliJ UI colors must come from the current theme, component state, editor color scheme, or a centralized semantic named color key.
Prefer semantic helpers for common UI roles:
| Need | Preferred API |
|---|---|
| Ordinary label text | UIUtil.getLabelForeground() |
| Secondary/help text | UIUtil.getContextHelpForeground() |
| Info label text | UIUtil.getLabelInfoForeground() |
| Success label text | UIUtil.getLabelSuccessForeground() |
| Error label text | UIUtil.getErrorForeground() |
| Warning label text | JBUI.CurrentTheme.Label.warningForeground() |
| Generic label foreground by state | JBUI.CurrentTheme.Label.foreground(selected) / disabledForeground(selected) |
| Inactive secondary text | NamedColorUtil.getInactiveTextColor() |
| Bounds and standard border color | NamedColorUtil.getBoundsColor() / JBColor.border() |
| Tooltips | UIUtil.getToolTipForeground() / getToolTipBackground() / JBUI.CurrentTheme.Tooltip.* |
| Links | JBUI.CurrentTheme.Link.Foreground.ENABLED / HOVERED / PRESSED / VISITED / DISABLED / SECONDARY |
| List renderer text/background | UIUtil.getListForeground(selected, focused) / UIUtil.getListBackground(selected, focused) |
| Tree renderer text/background | UIUtil.getTreeForeground(selected, focused) / UIUtil.getTreeBackground(selected, focused) |
| Non-selected tree content | UIUtil.getTreeForeground() / UIUtil.getTreeBackground() |
Prefer JBUI.CurrentTheme for component-area colors and borders:
| Need | Preferred API |
|---|---|
| Popup background | JBUI.CurrentTheme.Popup.BACKGROUND |
| Complex popup header | JBUI.CurrentTheme.ComplexPopup.HEADER_BACKGROUND |
| Separators | JBUI.CurrentTheme.CustomFrameDecorations.separatorForeground() |
| Advertiser/footer surfaces | JBUI.CurrentTheme.Advertiser.foreground() / background() / border() |
| Links | JBUI.CurrentTheme.Link.Foreground.ENABLED |
| Tree selection state | JBUI.CurrentTheme.Tree.foreground(...) and selection helpers |
| Validation errors | JBUI.CurrentTheme.Validator.errorBorderColor() / errorBackgroundColor() |
| Validation warnings | JBUI.CurrentTheme.Validator.warningBorderColor() / warningBackgroundColor() |
| Focus error/warning outlines | JBUI.CurrentTheme.Focus.errorColor(active) / warningColor(active) |
| Banner-like surfaces | JBUI.CurrentTheme.Banner.* |
| Notification tool-window surfaces | JBUI.CurrentTheme.NotificationError.*, NotificationWarning.*, NotificationInfo.* |
| Icon badges | JBUI.CurrentTheme.IconBadge.* |
Use JBColor.lazy { ... } for colors that depend on runtime state or the active editor color scheme:
val bg = JBColor.lazy { EditorColorsManager.getInstance().globalScheme.defaultBackground }
Use JBColor.namedColor("Some.Semantic.Key", fallback) only when defining or consuming a semantic color key. Prefer a fallback from an existing theme API over numeric RGB values in examples.
For HTML/CSS snippets, compute the color from a theme API and convert it with ColorUtil.toHtmlColor(...). Do not write hardcoded CSS color literals.
val fg = ColorUtil.toHtmlColor(UIUtil.getContextHelpForeground())
val html = HtmlChunk.span("color: $fg").addText(text)
Recommended examples:
label.applyToComponent {
foreground = UIUtil.getContextHelpForeground()
font = JBFont.medium()
}
foreground = if (selected) {
UIUtil.getTreeForeground(true, hasFocus)
} else {
UIUtil.getContextHelpForeground()
}
Avoid inline Color(...), numeric JBColor(...), Gray.xNN, JBColor.GRAY, and hex color literals in runtime UI code. The exception is a centralized semantic color definition with a named color key when no existing platform key exists.
Do not hardcode font sizes or font families. IntelliJ fonts must come from component defaults, platform typography helpers, or existing component fonts.
JBFont.h1(), JBFont.h2(), JBFont.h3(), and JBFont.h4() for headings..asBold(), .asItalic(), and .asPlain() for style changes on JBFont values.JBFont.regular(), JBFont.medium(), and JBFont.small() for regular and secondary text.UIUtil.getLabelFont(UIUtil.FontSize.SMALL) / MINI only when an API expects a raw Font and JBFont / RelativeFont is not a good fit.RelativeFont when adjusting an existing component font relatively.RelativeFont.fromResource(...) for theme-configurable font offsets when matching platform component patterns.JBUI.Fonts.smallFont() only when matching an existing platform component pattern that expects it.For errors, grayed text, shortcuts, and links in renderers, prefer SimpleTextAttributes.ERROR_ATTRIBUTES, GRAYED_ATTRIBUTES, SHORTCUT_ATTRIBUTES, and LINK_ATTRIBUTES rather than manually selecting colors and styles.
Recommended examples:
label(title).applyToComponent {
font = JBFont.h3().asBold()
}
RelativeFont.BOLD.install(label)
Avoid Font("..."), raw font sizes, and deriveFont(14f) style examples.
For session/chat UI under packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/, keep general UI chrome separate from transcript styling.
ai.kilocode.client.ui.UiStyle for general UI helpers: Colors, Borders, Insets, Gap, Space, Size, and Buttons.ai.kilocode.client.session.ui.SessionStyle for session transcript/content styling that follows editor settings or live transcript configuration.SessionStyle is for user/assistant transcript text, reasoning text, tool output, prompt editor text, and markdown renderer fields such as font, codeFont, and future link/code-block styles.SessionStyleTarget.applyStyle(style) instead of reading editor globals once in constructors.EditorColorsManager.getInstance().globalScheme directly from individual views unless extending SessionStyle.current().SessionStyle snapshot and ensure child parts created after a style change receive the queued style.Use UiStyle for the surrounding card chrome and SessionStyle for transcript content inside it:
class ExamplePartView : PartView() {
override fun applyStyle(style: SessionStyle) {
var changed = false
if (md.font != style.transcriptFont) {
md.font = style.transcriptFont
changed = true
}
if (md.codeFont != style.editorFamily) {
md.codeFont = style.editorFamily
changed = true
}
if (!changed) return
revalidate()
repaint()
}
}
JBUI.Borders.empty(top, left, bottom, right) and insets via JBUI.insets() so they are DPI-aware and auto-update on zoom.JBUI.scale(int) for any pixel dimension to ensure proper HiDPI scaling.EmptyBorder, raw Insets, or raw Dimension unless there is no platform alternative and the reason is obvious.Theme-dependent borders, insets, colors, and corner arcs must be re-evaluated when the Look and Feel changes. Do not assign a theme-derived border once in a constructor for a long-lived component.
Prefer overriding updateUI() for component-local updates:
class KiloPanel : JPanel() {
override fun updateUI() {
super.updateUI()
border = JBUI.Borders.customLine(
JBColor.namedColor("KiloPanel.borderColor", JBColor.border()),
1,
)
}
}
For long-lived tool-window panels or components that need explicit Look and Feel handling outside normal Swing updates, subscribe to LafManagerListener.TOPIC and reapply theme-dependent borders or cached UI state there.
Use JBValue.UIInteger for themeable arc values, because defaults differ by theme:
| Key | Use |
|---|---|
Component.arc | General component corners |
Button.arc | Button corners |
Popup.Selection.arc | Popup selection background |
TabbedPane.tabSelectionArc | Tab selection indicator |
Tree.Selection.arc | Tree selection background |
val arc = JBValue.UIInteger("Component.arc", 5).get()
val button = JBValue.UIInteger("Button.arc", 6).get()
Only define plugin-owned theme keys when a platform key is not enough. Themeable insets can use JBUI.insets("KiloPanel.insets", JBUI.insets(4, 8)); themeable colors should use JBColor.namedColor("KiloPanel.borderColor", fallback).
Themes can override plugin-owned UI keys through the ui section:
{
"ui": {
"KiloPanel.insets": "6,12,6,12",
"KiloPanel.gap": 20,
"KiloPanel.borderColor": "#FF0000",
"KiloPanel.arc": 10
}
}
Expose intentional custom UI keys to theme authors with the themeMetadataProvider extension point and a *.themeMetadata.json file.
<themeMetadataProvider path="/META-INF/Kilo.themeMetadata.json"/>
{
"name": "Kilo Code",
"fixed": false,
"ui": [
{ "key": "KiloPanel.borderColor", "description": "Panel border color" },
{ "key": "KiloPanel.gap", "description": "Gap between panel items" }
]
}
Prefer built-in validation mechanisms over custom error rendering.
validationOnInput, validationOnApply, validationInfo, errorOnApply, and addValidationRule on cells.DialogPanel.registerValidators(disposable) when input validation should run continuously.DialogPanel.validateAll() before apply() when submitting a DSL panel manually.DialogWrapper, use doValidate() and return ValidationInfo(message, component) for errors.ValidationInfo.asWarning() for warnings and withOKEnabled() when a warning should not block submission.ComponentValidator with withValidator, withFocusValidator, and andRegisterOnDocumentListener instead of custom tooltip/error border logic.JBUI.CurrentTheme.Validator.* and JBUI.CurrentTheme.Focus.* colors.Kotlin UI DSL example:
textField()
.validationOnInput {
if (it.text.isBlank()) error(KiloBundle.message("settings.name.required")) else null
}
Dialog validation example:
override fun doValidate(): ValidationInfo? {
if (field.text.isBlank()) return ValidationInfo(KiloBundle.message("settings.name.required"), field)
return null
}
AllIcons.* constants.resources/icons/.IconLoader.getIcon("/icons/foo.svg", MyClass::class.java).icons package or a *Icons object with @JvmField on each constant.icon.svg + icon_dark.svg.[email protected] + icon@2x_dark.svg.expui/, create *IconMappings.json, register via com.intellij.iconMapper extension point.IntelliJ does not theme SVG icons with currentColor, CSS classes, CSS variables, inherited styles, or <style> blocks. SVGLoader patches icon colors by matching literal hex values in fill and stroke attributes against the active theme palette.
Use hardcoded palette hex values in SVG assets and provide dark variants. This exception applies to icon asset files only; runtime Swing UI code must still derive colors from theme APIs.
| Do | Do not |
|---|---|
fill="#6E6E6E" | fill="currentColor" |
stroke="#3574F0" | CSS variables or classes |
icon.svg plus icon_dark.svg | <style> blocks for theming |
fill-opacity="0.5" | Inherited styling |
Classic icon palette examples:
| Palette key | Light hex | Dark hex |
|---|---|---|
Actions.Grey | #6E6E6E | #AFB1B3 |
Actions.Red | #DB5860 | #C75450 |
Actions.Green | #59A869 | #499C54 |
Actions.Blue | #389FD6 | #3592C4 |
Objects.Grey | #9AA7B0 | |
Objects.Blue | #40B6E0 |
New UI palette examples:
| Palette key | Light hex | Dark hex |
|---|---|---|
Gray.Stroke | #6C707E | #CED0D6 |
Gray.Fill | #EBECF0 | #43454A |
Gray.SecondaryStroke | #A8ADBD | #6F737A |
Blue.Stroke | #3574F0 | #548AF7 |
Blue.Fill | #EDF3FF | #25324D |
Blue.Solid | #4682FA | |
Green.Stroke | #208A3C | #57965C |
Red.Stroke | #DB3B4B | #DB5C5C |
For checkbox, radio, or similar control SVGs that need independent fill and stroke theming, use an id in the fillKey_strokeKey format. The SVG patcher splits the ID on _ and applies the keys independently.
<rect id="Checkbox.Background.Selected_Checkbox.Border.Selected"
x="4.5" y="4.5" width="15" height="15" rx="2.5"
fill="#3574F0" stroke="#3574F0"/>
Themes can override palette colors through icons.ColorPalette:
{
"icons": {
"ColorPalette": {
"Actions.Grey": "#A8ADBD",
"Blue.Stroke": "#548AF7"
}
}
}
<notificationGroup id="Kilo Code" displayType="BALLOON"/>.Notification("Kilo Code", "message", NotificationType.INFORMATION).notify(project)..addAction(NotificationAction.createSimpleExpiring("Label") { ... }).displayType="STICKY_BALLOON" and .setSuggestionType(true).displayType="TOOL_WINDOW" toolWindowId="Kilo Code".Messages.show*() dialogs.JBPopupFactory.getInstance() for lightweight floating UI with no chrome and auto-dismiss on focus loss.createComponentPopupBuilder(component, focusable) for arbitrary Swing content.createPopupChooserBuilder(list) for item selection.createActionGroupPopup() for action menus.showInBestPositionFor(editor), showUnderneathOf(component), or showInCenterOf(component).Use this guidance when building native list pickers that need richer presentation than a simple action menu. Common cases include two-line rows with title and description, active/current item checkmarks, inactive icon placeholders for alignment, hover behaving like selection, optional inline actions such as favorites, and optional filtering or search.
ListPopupImpl / BaseListPopupStep are fine for simple menu-like lists. Do not make them the main abstraction for reusable rich list UIs because they are popup-first and action-step oriented.
JBList<T> for the list.CollectionListModel<T> for simple item storage.FilteringListModel<T> or a small custom AbstractListModel<T> for filtering and ranking.TreeUIHelper.getInstance().installListSpeedSearch(list) { ... } for speed search on current IntelliJ platform APIs.ListUtil.installAutoSelectOnMouseMove(list) for IntelliJ popup-like hover selection.ScrollingUtil.installActions(list) for keyboard navigation.JBPopupFactory.createComponentPopupBuilder(component, preferredFocusComponent) for popup wrapping.ScrollPaneFactory.createScrollPane(list) or JBScrollPane for scrolling.SimpleColoredComponent, SimpleTextAttributes, JBLabel, and JPanel for custom renderers.AllIcons.Actions.Checked and EmptyIcon.create(...) for active-item alignment.Use layout managers for sizing and alignment. Avoid preferredSize, maximumSize, and manual pixel sizing for normal badge or row layout. A good rich row renderer structure is:
JPanel(BorderLayout()).JBLabel with icon centered horizontally and vertically.JPanel(BorderLayout()).FlowLayout(FlowLayout.LEFT, 0, 0) with title first, then optional tag/badge.java.awt.Component as a renderer return type; return the concrete Swing component such as JPanel.private class PickerRenderer<T>(
private val active: () -> T?,
) : JPanel(BorderLayout()), ListCellRenderer<T> {
private val icon = JBLabel().apply {
isOpaque = false
horizontalAlignment = SwingConstants.CENTER
verticalAlignment = SwingConstants.CENTER
}
private val title = SimpleColoredComponent().apply { isOpaque = false }
private val description = SimpleColoredComponent().apply { isOpaque = false }
private val tag = JBLabel().apply { isOpaque = false }
private val header = JPanel(FlowLayout(FlowLayout.LEFT, 0, 0)).apply {
isOpaque = false
add(title)
add(tag)
}
private val body = JPanel(BorderLayout()).apply {
isOpaque = false
add(header, BorderLayout.NORTH)
add(description, BorderLayout.CENTER)
}
init {
isOpaque = true
add(icon, BorderLayout.WEST)
add(body, BorderLayout.CENTER)
}
override fun getListCellRendererComponent(
list: JList<out T>,
value: T,
index: Int,
selected: Boolean,
focused: Boolean,
): JPanel {
val focus = list.hasFocus() || focused
background = UIUtil.getListBackground(selected, focus)
val fg = UIUtil.getListForeground(selected, focus)
val weak = if (selected) fg else UIUtil.getContextHelpForeground()
// Clear and append title/description fragments here.
return this
}
}
EmptyIcon.create(checkIcon) for inactive items so text aligns.ListUtil.installAutoSelectOnMouseMove(list) so hover and selection are the same UI state.UIUtil.getListForeground(selected, focused) and UIUtil.getListBackground(selected, focused), or ListUiUtil.WithTallRow when available and appropriate.JBPopupFactory.createComponentPopupBuilder(component, focusComponent).setRequestFocus(true), setCancelOnClickOutside(true), setCancelKeyEnabled(true), and usually setMovable(false).popup.closeOk(null) after activation.showUnderneathOf(component), showInBestPositionFor(editor), or PopupShowOptions.aboveComponent(component) depending on the trigger context.locationToIndex and getCellBounds.ListWithFilter.wrap(...) when a simple searchable text representation is enough.SearchTextField plus FilteringListModel or a small custom AbstractListModel when custom matching/ranking is needed.list.emptyText.text.ListPopupImpl as the main abstraction for reusable rich list components.preferredSize / maximumSize to force normal row or badge layout; fix the layout manager instead.ListUtil.installAutoSelectOnMouseMove: https://github.com/JetBrains/intellij-community/blob/master/platform/platform-api/src/com/intellij/ui/ListUtil.javaPopupChooserBuilder flow: https://github.com/JetBrains/intellij-community/blob/master/platform/platform-api/src/com/intellij/openapi/ui/popup/PopupChooserBuilder.javaListWithFilter: https://github.com/JetBrains/intellij-community/blob/master/platform/platform-api/src/com/intellij/ui/speedSearch/ListWithFilter.javaPopupListElementRenderer: https://github.com/JetBrains/intellij-community/blob/master/platform/platform-impl/src/com/intellij/ui/popup/list/PopupListElementRenderer.javaListUiUtil.WithTallRow: https://github.com/JetBrains/intellij-community/blob/master/platform/util/ui/src/com/intellij/util/ui/ListUiUtil.ktJBList, not JList, for empty text, busy indicator, and tooltip truncation.Tree, not JTree, for wide selection painting and auto-scroll on drag-and-drop.ColoredListCellRenderer / ColoredTreeCellRenderer for custom renderers.append() for styled text and setIcon() for icons.ListSpeedSearch(list) / TreeSpeedSearch(tree) for speed search.ToolbarDecorator.createDecorator(list).setAddAction { }.setRemoveAction { }.createPanel() for editable lists with add/remove/reorder toolbar.Review generated UI code and remove:
isOpaque = falsepreferredSize, minimumSize, or maximumSizeDimension, Insets, EmptyBorder, or ColorColor(...), numeric JBColor(...), Gray.xNN, JBColor.GRAY, or hex color literalsColorUtil.toHtmlColor(themeColor) is allowed because the source color is theme-derivedderiveFont(...) callsJBUI valueJBValue.UIInteger(...).get() values in custom layouts; call .get() during layout and sizing insteadupdateUI() or Look and Feel handlingcurrentColor, CSS variables, CSS classes, <style> blocks, or inherited styling for IntelliJ icon theming