mirror of
https://github.com/Kilo-Org/kilocode.git
synced 2026-08-29 03:44:06 +08:00
fix(jetbrains): keep session popups inside the visible view
Header popups budgeted their height against the IDE layered pane and anchored on the session panel's full bounds, so a session in a short tool window or an editor tab got balloons spanning the whole window. Placement now uses the session panel's visible rect for the height budget and the vertical clamp, mirroring SessionHoverCopyOverlay. The pane still decides only which side has horizontal room: clamping the width to the view rect would collapse the popup, since it deliberately sits beside the session.
This commit is contained in:
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"@kilocode/kilo-jetbrains": patch
|
||||
---
|
||||
|
||||
Show worktree session titles in regular weight, keep the account switcher hidden when a new worktree starts with a prompt, add new worktrees at the top of the Agent Manager list, keep the running indicator on worktree rows when a stopped session is resumed, and keep session card popups inside the visible session view.
|
||||
+10
-6
@@ -161,8 +161,9 @@ class HeaderPopupController(timers: UiTimerSource = UiTimers) : Disposable {
|
||||
|
||||
/**
|
||||
* Resolves the pointer target beside the session chat, sizing the body to the space available on
|
||||
* the chosen side. Anchoring on the chat rather than the hovered row is what keeps the popup off
|
||||
* the transcript instead of covering the row the user is reading.
|
||||
* the chosen side and to the visible height of the chat. Anchoring on the chat rather than the
|
||||
* hovered row is what keeps the popup off the transcript instead of covering the row the user is
|
||||
* reading.
|
||||
*
|
||||
* Returns null when the chat is not on screen yet, in which case there is nothing to sit beside.
|
||||
*/
|
||||
@@ -177,10 +178,13 @@ class HeaderPopupController(timers: UiTimerSource = UiTimers) : Disposable {
|
||||
// The shadow is reserved on every side, so it counts twice on each axis.
|
||||
val shadow = UiStyle.Balloon.shadow() * 2
|
||||
val chromeHeight = insets.top + insets.bottom + shadow
|
||||
val bounds = Rectangle(pane.size)
|
||||
// The visible chat rect, not the whole panel: a session clipped by a short tool window or a
|
||||
// scrolled editor tab must keep its popups inside the part the user can actually see.
|
||||
val area = SwingUtilities.convertRectangle(chat, chat.visibleRect, pane)
|
||||
if (area.isEmpty) return null
|
||||
val spot = HeaderPopupGeometry.beside(
|
||||
pane = bounds,
|
||||
chat = SwingUtilities.convertRectangle(chat.parent, chat.bounds, pane),
|
||||
pane = Rectangle(pane.size),
|
||||
chat = area,
|
||||
fit = HeaderPopupFit(
|
||||
chromeWidth = insets.left + insets.right + UiStyle.Balloon.pointer().height + shadow,
|
||||
chromeHeight = chromeHeight,
|
||||
@@ -192,7 +196,7 @@ class HeaderPopupController(timers: UiTimerSource = UiTimers) : Disposable {
|
||||
built.fitWithin(spot.maxWidth, spot.maxHeight)
|
||||
val row = SwingUtilities.convertPoint(anchor, Point(0, anchor.height / 2), pane)
|
||||
val height = built.component.preferredSize.height + chromeHeight
|
||||
return Spot(pane, Point(spot.x, HeaderPopupGeometry.centerY(bounds, row.y, height, gap)), spot.position)
|
||||
return Spot(pane, Point(spot.x, HeaderPopupGeometry.centerY(area, row.y, height, gap)), spot.position)
|
||||
}
|
||||
|
||||
private class Spot(val pane: JComponent, val point: Point, val position: Balloon.Position)
|
||||
|
||||
+14
-7
@@ -40,7 +40,12 @@ internal data class HeaderPopupFit(
|
||||
*/
|
||||
internal object HeaderPopupGeometry {
|
||||
|
||||
/** Picks the side of [chat] with more room inside [pane] and the body box that fits there. */
|
||||
/**
|
||||
* Picks the side of [chat] with more room inside [pane] and the body box that fits there.
|
||||
*
|
||||
* [pane] only decides which side has room; the body is bounded by [chat] so the popup stays
|
||||
* within the visible session view.
|
||||
*/
|
||||
fun beside(pane: Rectangle, chat: Rectangle, fit: HeaderPopupFit): HeaderPopupPlacement {
|
||||
val left = (chat.x - pane.x).coerceAtLeast(0)
|
||||
val right = (pane.x + pane.width - (chat.x + chat.width)).coerceAtLeast(0)
|
||||
@@ -51,20 +56,22 @@ internal object HeaderPopupGeometry {
|
||||
position = if (useRight) Balloon.Position.atRight else Balloon.Position.atLeft,
|
||||
x = if (useRight) chat.x + chat.width else chat.x,
|
||||
maxWidth = room.coerceIn(0, fit.maxWidth),
|
||||
maxHeight = (pane.height - fit.gap * 2 - fit.chromeHeight).coerceIn(0, fit.maxHeight),
|
||||
// Height is budgeted against the chat, not the pane: the popup belongs to the session
|
||||
// view, so it must not run past it into editor tabs or neighbouring tool windows.
|
||||
maxHeight = (chat.height - fit.gap * 2 - fit.chromeHeight).coerceIn(0, fit.maxHeight),
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Vertical pointer target for a body of [height], preferring [y] but keeping the balloon inside
|
||||
* [pane]. The balloon centres its body on the target, so an unclamped target near an edge would
|
||||
* [chat]. The balloon centres its body on the target, so an unclamped target near an edge would
|
||||
* overflow and trigger the same re-pointing that [beside] avoids horizontally.
|
||||
*/
|
||||
fun centerY(pane: Rectangle, y: Int, height: Int, gap: Int): Int {
|
||||
fun centerY(chat: Rectangle, y: Int, height: Int, gap: Int): Int {
|
||||
val half = height / 2
|
||||
val top = pane.y + gap + half
|
||||
val bottom = pane.y + pane.height - gap - half
|
||||
if (bottom < top) return pane.y + pane.height / 2
|
||||
val top = chat.y + gap + half
|
||||
val bottom = chat.y + chat.height - gap - half
|
||||
if (bottom < top) return chat.y + chat.height / 2
|
||||
return y.coerceIn(top, bottom)
|
||||
}
|
||||
}
|
||||
|
||||
+34
-13
@@ -93,34 +93,55 @@ class HeaderPopupGeometryTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `height is capped to the pane minus gaps`() {
|
||||
fun `height is capped to the chat minus gaps`() {
|
||||
val short = HeaderPopupGeometry.beside(
|
||||
pane = Rectangle(0, 0, 2000, 200),
|
||||
chat = Rectangle(0, 0, 300, 200),
|
||||
fit = fit(),
|
||||
)
|
||||
|
||||
// 200 pane, minus both gaps and the chrome the balloon reserves vertically.
|
||||
// 200 chat, minus both gaps and the chrome the balloon reserves vertically.
|
||||
assertEquals(200 - GAP * 2 - CHROME_HEIGHT, short.maxHeight)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `pointer target keeps a tall body inside the pane`() {
|
||||
val pane = Rectangle(0, 0, 2000, 1000)
|
||||
fun `height follows a short chat inside a tall pane`() {
|
||||
// Session in an editor tab or a short tool window: the window has room the session does not.
|
||||
val spot = HeaderPopupGeometry.beside(
|
||||
pane = Rectangle(0, 0, 2000, 1000),
|
||||
chat = Rectangle(0, 100, 300, 300),
|
||||
fit = fit(),
|
||||
)
|
||||
|
||||
// Row near the top: target pushed down so the centred body clears the top edge.
|
||||
assertEquals(310, HeaderPopupGeometry.centerY(pane, y = 20, height = 600, gap = GAP))
|
||||
// Row near the bottom: target pulled up.
|
||||
assertEquals(690, HeaderPopupGeometry.centerY(pane, y = 980, height = 600, gap = GAP))
|
||||
// Row with room on both sides is left alone.
|
||||
assertEquals(500, HeaderPopupGeometry.centerY(pane, y = 500, height = 600, gap = GAP))
|
||||
assertEquals(300 - GAP * 2 - CHROME_HEIGHT, spot.maxHeight)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `body taller than the pane is centred instead of clamped to an empty range`() {
|
||||
val pane = Rectangle(0, 0, 2000, 400)
|
||||
fun `pointer target keeps the body inside an offset chat`() {
|
||||
val chat = Rectangle(0, 400, 300, 400)
|
||||
|
||||
assertEquals(200, HeaderPopupGeometry.centerY(pane, y = 10, height = 900, gap = GAP))
|
||||
// Rows above and below the chat are pulled back into it.
|
||||
assertEquals(560, HeaderPopupGeometry.centerY(chat, y = 0, height = 300, gap = GAP))
|
||||
assertEquals(640, HeaderPopupGeometry.centerY(chat, y = 1000, height = 300, gap = GAP))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `pointer target keeps a tall body inside the chat`() {
|
||||
val chat = Rectangle(0, 0, 300, 1000)
|
||||
|
||||
// Row near the top: target pushed down so the centred body clears the top edge.
|
||||
assertEquals(310, HeaderPopupGeometry.centerY(chat, y = 20, height = 600, gap = GAP))
|
||||
// Row near the bottom: target pulled up.
|
||||
assertEquals(690, HeaderPopupGeometry.centerY(chat, y = 980, height = 600, gap = GAP))
|
||||
// Row with room on both sides is left alone.
|
||||
assertEquals(500, HeaderPopupGeometry.centerY(chat, y = 500, height = 600, gap = GAP))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `body taller than the chat is centred instead of clamped to an empty range`() {
|
||||
val chat = Rectangle(0, 0, 300, 400)
|
||||
|
||||
assertEquals(200, HeaderPopupGeometry.centerY(chat, y = 10, height = 900, gap = GAP))
|
||||
}
|
||||
|
||||
private fun beside(chat: Rectangle) = HeaderPopupGeometry.beside(
|
||||
|
||||
Reference in New Issue
Block a user