Skip to content

fix(jetbrains): rescale Agent Manager list rows on IDE zoom - #13635

Merged
kirillk merged 2 commits into
mainfrom
swift-quartz
Sep 1, 2026
Merged

fix(jetbrains): rescale Agent Manager list rows on IDE zoom#13635
kirillk merged 2 commits into
mainfrom
swift-quartz

Conversation

@kirillk

@kirillk kirillk commented Sep 1, 2026

Copy link
Copy Markdown
Contributor

Issue

Fixes # — no filed issue. Reported directly while testing the JetBrains plugin: the Agent Manager list does not scale correctly when using the global IntelliJ interface zoom.

Context

Changing the global IntelliJ interface zoom (View | Appearance | Zoom IDE In/Out) left the Agent Manager and session-history lists with their pre-zoom geometry.

The text did scale, which made the breakage look partial rather than total: a JBFont re-derives its size from Label.font on every read (JBFont.getSize()refreshScaledFont()), so fonts follow a zoom on their own. Everything else in a row — theme insets, layout gaps, BorderLayout.hgap, and the measured row height — is a plain pixel count resolved once through JBUI.scale. Those stayed put, so the section header band and the row metrics drifted out of alignment with text that had grown.

Implementation

Root cause: the shared row renderer never received the platform's Look-and-Feel pass.

javax.swing.JList.updateUI only forwards to its renderer when the renderer is itself a Component:

// JList.java:573
ListCellRenderer<? super E> renderer = getCellRenderer();
if (renderer instanceof Component) {
    SwingUtilities.updateComponentTreeUI((Component)renderer);
}

JBList.setCellRenderer wraps whatever it is given in ExpandedItemListCellRendererWrapper, which implements ListCellRenderer but is not a Component. The renderer is also not a child of the list, so IJSwingUtilities.updateComponentTreeUI(frame) never reaches it either. ActiveListRenderer.updateUI() was therefore never called on a zoom.

ActiveListView now refreshes the stamp itself and drops its measured-height cache. That cache is keyed on (config, list.width, rows) — a zoom changes none of those, so it compared equal and syncCellHeight kept the pre-zoom height.

All DPI-derived values in the renderer moved into one syncScale() applied from both the constructors and updateUI(), since a layout manager captures its gap and an assigned border its insets at creation time.

Also fixes double scaling on these paths. JBUI.Borders scales what it is handed (JBEmptyBorderJBInsets, whose javadoc says "You should pass unscaled values only!"), so passing an already-scaled UiStyle.Gap.md() applied the factor twice — invisible at 100%, drifting as soon as the IDE is zoomed. UiStyle.Gap gains unscaled constants for those call sites, and DiffStatBadge's inset parameter becomes an unscaled step.

Two supporting changes reviewers may want to look at:

  • Stack gains a settable space, so a DPI-derived gap captured by its layout manager can be re-derived rather than frozen.
  • rescale() measures twice: once inline and once via invokeLater. IJSwingUtilities.updateComponentTreeUI visits children before parents, so when list.updateUI() runs the list's ancestors still hold pre-zoom theme values. The deferred pass lets the tree settle at the new scale first — the same ordering HistoryPanel.bindTheme() already relies on.

Scope note: the same JBUI.Borders.empty(UiStyle.Gap.xx()) double-scaling pattern exists at ~90 other call sites across the plugin. Only the ones on the list/Agent Manager path are fixed here; the plugin-wide sweep is mechanical, carries real visual-regression risk, and belongs in its own PR.

Things I got wrong while investigating, in case it saves a reviewer the same detour: I initially assumed font = JBFont.small() was frozen in ChangesPanel, DiffStatBadge, and FilledBadgeIcon. It is not — JBFont self-rescales, and re-applying the font in updateUI() would also have clobbered the font hosts push in through PrHeaderView.applyStyle. Those files therefore only get their genuinely-frozen parts fixed: layout gaps, iconTextGap, and the double-scaled borders.

Screenshots / Video

Before

Screen Shot 2026-09-01 at 8 20 58 AM

After

Screen Shot 2026-08-31 at 10 30 28 PM

How to Test

Manual/local verification

  • ./gradlew typecheck test --rerun-tasks from packages/kilo-jetbrains/ — green across shared, frontend, and backend. Agent-executed.

  • CI green on this branch, including jetbrains / jetbrains (4593 tests). One unrelated flake, SessionUpdateQueueTest > test update hooks run on EDT around history and recovery, failed once and then passed on re-run with no code change; it does not touch this diff. The --rerun-tasks matters: Gradle served a stale instrumented jar during intermediate runs and briefly reported false results, so every verification below was done with a forced rebuild.

  • Instrumented the real AgentManagerPanel (main row plus a worktree row with files=14, +742/-169, ahead=2, behind=41 and a PR badge — the tallest row shape the panel renders) and drove it through repeated zooms, applying only what the platform does: raise the *.font defaults, raise the user scale, then IJSwingUtilities.updateComponentTreeUI(panel). Agent-executed:

    ZOOM[100%] scale=1.0  label=13 rowPref=56 cell=56
    ZOOM[125%] scale=1.25 label=16 rowPref=70 cell=70
    ZOOM[100%] scale=1.0  label=13 rowPref=56 cell=56
    ZOOM[125%] scale=1.25 label=16 rowPref=70 cell=70
    ZOOM[100%] scale=1.0  label=13 rowPref=56 cell=56
    
  • Confirmed test renderer layout gaps are re derived after a zoom fails without the renderer refresh and passes with it, each direction on a forced rebuild. Agent-executed.

Reviewer test steps

  1. Build and launch the plugin (./gradlew runIde from packages/kilo-jetbrains/).
  2. Open the Kilo tool window and switch to the Agents tab so several worktrees are listed, ideally including some with diff metrics and PR badges.
  3. Zoom in with View | Appearance | Zoom IDE In (or set a zoom level in Settings | Appearance).
  4. Confirm row height, row padding, the "Local worktrees" section header band, and the N files -x +y metrics all grow in proportion to the text.
  5. Zoom back out to 100% and confirm every one of those returns to its original size.

Blocked checks and substitute verification

  • Not fully verified. The reporter still sees row padding grow and not return after zooming back out, on a build that includes these changes. I could not reproduce that in-process: the faithful simulation above round-trips exactly, across two full zoom cycles, on the real panel.

    What I ruled out: the platform ordering is correct (LafManagerImpl.updateUI runs patchLafFonts, which sets the font defaults and JBUIScale.setUserScaleFactor, at line 823 — before notifyLookAndFeelChanged at 843 and the tree walk at 844), and patchHiDPI is idempotent for already-converted values.

    One detail argues for something accumulating rather than a single mis-scaled value: the reported rows are roughly 170px tall, while the instrumented panel measures 56px at 100% and 70px at 125%. Even 200% would only reach ~112px.

    So this PR is a real, test-covered fix for a real defect on this path, but it may not be the whole story. Landing it separately from the remaining investigation keeps the verified part reviewable, and the new ActiveListScaleTest gives the follow-up a place to add a failing case. Happy to hold this as a draft if a reviewer would rather wait for the full root cause.

  • A promising lead for that follow-up, surfaced by CI. The first CI run failed the three "shrinks back" assertions on Linux with the height stuck at its zoomed value (expected:<47> but was:<74>) while passing locally on macOS — the reporter's symptom exactly. The cause turned out to be in the test rather than the product: the zoom helper installed a plain Font into the UI defaults, and LookAndFeel.installColorsAndFont only replaces a component font that is null or a UIResource, so components adopted the zoomed font once and then refused every later update. LafManagerImpl.patchLafFonts installs FontUIResource values; the helpers now do the same (second commit) and CI is green.

    That mechanism is worth carrying into the follow-up. Any component in the row tree holding a non-UIResource, non-self-rescaling font would be permanently pinned to the size it was assigned at — growing on one zoom and never shrinking. RelativeFont.derive is a concrete candidate: handed a UIResource it re-wraps its result in a plain FontUIResource (RelativeFont.java:179-181), losing JBFont's self-rescaling, and ActiveListRenderer.syncHeader derives the section caption font from sep.font on every render — so once that font is a plain FontUIResource the caption is pinned. Whether sep.font ever becomes a UIResource is LaF-dependent, which would explain why this reproduces for the reporter and not for me. I did not change it here: I tried resetting that font earlier in this branch and it visibly altered the caption size (12px to 11px), so it needs a verified repro rather than another speculative edit.

  • No screenshots of the visual result, for the reason given in that section.

Checklist

  • Issue linked above, or exception explained
  • Tests/verification described
  • Screenshots/video included for visual changes, or marked N/A
  • Changeset considered for user-facing changes
  • I personally reviewed the diff and can explain the changes, including any AI-assisted work.

Get in Touch

The Agent Manager and session-history lists kept their pre-zoom geometry
when the global IntelliJ interface zoom changed. Text grew because JBFont
re-derives its size from "Label.font" on every read, but the theme insets,
layout gaps and measured row heights around it are plain pixel counts that
were never recomputed, so headers and row metrics drifted out of alignment.

The shared row renderer never received the platform's Look-and-Feel pass.
JList.updateUI only forwards to its renderer `if (renderer instanceof
Component)`, and JBList wraps whatever it is given in a non-Component
adapter, so the stamp was unreachable. The list now refreshes the stamp
itself and drops its measured-height cache, which is otherwise keyed only
on row data and list width and therefore compares equal across a zoom.

Also fixes double scaling on the affected paths: JBUI.Borders scales what
it is handed, so passing an already-scaled UiStyle.Gap value applied the
factor twice. Gap gains unscaled constants for those call sites, Stack
gains a settable base gap so a DPI-derived gap captured by a layout
manager can be re-derived, and DiffStatBadge's `inset` becomes an unscaled
step.
@kilo-code-bot

kilo-code-bot Bot commented Sep 1, 2026

Copy link
Copy Markdown
Contributor

Code Review Summary

Status: No Issues Found | Recommendation: Merge

Files Reviewed (16 files)
  • .changeset/jetbrains-agent-manager-zoom-scaling.md
  • packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/agentManager/AgentManagerPanel.kt
  • packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/diff/KiloDiffEditorContent.kt
  • packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/ui/ChangesPanel.kt
  • packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/ui/DiffStatBadge.kt
  • packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/ui/FilledBadgeIcon.kt
  • packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/ui/UiStyle.kt
  • packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/ui/layout/Stack.kt
  • packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/ui/list/ActiveList.kt
  • packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/ui/list/ActiveListRenderer.kt
  • packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/ui/list/ActiveListView.kt
  • packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/agentManager/AgentManagerPanelTest.kt
  • packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/ui/FilledBadgeIconTest.kt
  • packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/ui/UiStyleTest.kt
  • packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/ui/layout/StackTest.kt
  • packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/ui/list/ActiveListScaleTest.kt
Previous Review Summary (commit 037ae10)

Current summary above is authoritative. Previous snapshots are kept for context only.

Previous review (commit 037ae10)

Status: No Issues Found | Recommendation: Merge

Files Reviewed (16 files)
  • .changeset/jetbrains-agent-manager-zoom-scaling.md
  • packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/agentManager/AgentManagerPanel.kt
  • packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/diff/KiloDiffEditorContent.kt
  • packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/ui/ChangesPanel.kt
  • packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/ui/DiffStatBadge.kt
  • packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/ui/FilledBadgeIcon.kt
  • packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/ui/UiStyle.kt
  • packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/ui/layout/Stack.kt
  • packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/ui/list/ActiveList.kt
  • packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/ui/list/ActiveListRenderer.kt
  • packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/ui/list/ActiveListView.kt
  • packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/agentManager/AgentManagerPanelTest.kt
  • packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/ui/FilledBadgeIconTest.kt
  • packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/ui/UiStyleTest.kt
  • packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/ui/layout/StackTest.kt
  • packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/ui/list/ActiveListScaleTest.kt

Reviewed by grok-4.6 · Input: 210.2K · Output: 8.3K · Cached: 182.5K

Review guidance: REVIEW.md from base branch main

The zoom helpers put a plain Font into the UI defaults. LookAndFeel's
installColorsAndFont only replaces a component font that is null or a
UIResource, so components adopted the zoomed font once and then refused
every later update — the row height grew and never came back, failing the
round-trip assertions on Linux CI while passing locally.

LafManagerImpl.patchLafFonts installs FontUIResource values, so the
helpers now do the same and the simulation matches the platform.
@kirillk
kirillk enabled auto-merge September 1, 2026 12:23
@kirillk
kirillk merged commit b4621b7 into main Sep 1, 2026
29 of 32 checks passed
@kirillk
kirillk deleted the swift-quartz branch September 1, 2026 13:13
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants