mirror of
https://github.com/mark3labs/kit.git
synced 2026-06-14 03:30:26 +00:00
fix(ui): correct mouse selection Y-offset for reasoning blocks
The getItemAndLineAtY() method was using item.Height() which returns 0 for reasoning blocks (StreamingMessageItem with role='reasoning') because their render cache is intentionally never populated (they include a live duration timer). This caused all items below a reasoning block to have incorrect Y coordinates — clicking on the reasoning text would highlight the assistant text below it instead. Two fixes: 1. getItemAndLineAtY() now uses renderedHeight() which calls Render() and counts lines — matching exactly what View() does. This is the single source of truth for item height during hit-testing. 2. StreamingMessageItem.Height() now falls back to Render(0) when cachedRender is empty, fixing the same issue for other callers (GotoBottom, ScrollBy, clampOffset, etc.).
This commit is contained in:
@@ -172,10 +172,17 @@ func (s *StreamingMessageItem) Render(width int) string {
|
||||
|
||||
// Height returns the number of lines.
|
||||
func (s *StreamingMessageItem) Height() int {
|
||||
if s.cachedRender == "" {
|
||||
// For reasoning blocks, cachedRender is never populated (rendering is
|
||||
// width-independent and includes a live timer). Fall back to Render(0)
|
||||
// so callers always get the correct height.
|
||||
rendered := s.cachedRender
|
||||
if rendered == "" {
|
||||
rendered = s.Render(0)
|
||||
}
|
||||
if rendered == "" {
|
||||
return 0
|
||||
}
|
||||
return strings.Count(s.cachedRender, "\n") + 1
|
||||
return strings.Count(rendered, "\n") + 1
|
||||
}
|
||||
|
||||
// AppendChunk adds a content chunk and invalidates the render cache.
|
||||
|
||||
@@ -284,6 +284,10 @@ func (s *ScrollList) selectLine(itemIdx, lineIdx int) {
|
||||
// getItemAndLineAtY converts a viewport-relative Y coordinate to item index
|
||||
// and line index within that item. Accounts for scroll offset and item gaps.
|
||||
// Returns (-1, -1) if Y is outside the viewport or beyond all items.
|
||||
//
|
||||
// IMPORTANT: Uses Render()+line counting (not Height()) to compute item height,
|
||||
// because Height() on some MessageItem implementations (e.g. StreamingMessageItem
|
||||
// for reasoning blocks) may return 0 when the render cache is empty.
|
||||
func (s *ScrollList) getItemAndLineAtY(y int) (itemIdx, lineIdx int) {
|
||||
if y < 0 || y >= s.height || len(s.items) == 0 {
|
||||
return -1, -1
|
||||
@@ -292,7 +296,8 @@ func (s *ScrollList) getItemAndLineAtY(y int) (itemIdx, lineIdx int) {
|
||||
currentY := 0
|
||||
for idx := s.offsetIdx; idx < len(s.items); idx++ {
|
||||
item := s.items[idx]
|
||||
itemHeight := item.Height()
|
||||
// Compute height the same way View() does: render, then count lines.
|
||||
itemHeight := s.renderedHeight(item)
|
||||
|
||||
// Account for partial visibility of the first item.
|
||||
startLine := 0
|
||||
@@ -667,6 +672,18 @@ func (s *ScrollList) clampOffset() {
|
||||
}
|
||||
}
|
||||
|
||||
// renderedHeight returns the height of a message item in lines by actually
|
||||
// rendering it. This is the single source of truth for item height — it
|
||||
// matches exactly what View() produces, unlike item.Height() which may
|
||||
// return stale/zero values for uncached items (e.g. reasoning blocks).
|
||||
func (s *ScrollList) renderedHeight(item MessageItem) int {
|
||||
rendered := item.Render(s.width)
|
||||
if rendered == "" {
|
||||
return 0
|
||||
}
|
||||
return strings.Count(rendered, "\n") + 1
|
||||
}
|
||||
|
||||
// abs returns the absolute value of x.
|
||||
func abs(x int) int {
|
||||
if x < 0 {
|
||||
|
||||
Reference in New Issue
Block a user