From 17de9c79dbd908b2d988a736da74d558ba942c7b Mon Sep 17 00:00:00 2001 From: Ayman Bagabas Date: Wed, 3 Dec 2025 16:09:01 -0500 Subject: [PATCH] refactor(list): remove item IDs for simpler API The chat component can wrap the items with an identifiable interface. --- internal/ui/list/example_test.go | 29 ++-- internal/ui/list/item.go | 35 +--- internal/ui/list/item_test.go | 111 ++++++------ internal/ui/list/list.go | 286 ++++++++++++------------------- internal/ui/list/list_test.go | 100 +++++------ internal/ui/model/chat.go | 31 ++-- internal/ui/model/ui.go | 2 +- 7 files changed, 238 insertions(+), 356 deletions(-) diff --git a/internal/ui/list/example_test.go b/internal/ui/list/example_test.go index d88a616ddb7a58d547c55fb72e46f7fab31a9ec8..e656fe059f16d98db84cccb8af328ffdc92864c1 100644 --- a/internal/ui/list/example_test.go +++ b/internal/ui/list/example_test.go @@ -12,15 +12,15 @@ import ( func Example_basic() { // Create some items items := []list.Item{ - list.NewStringItem("1", "First item"), - list.NewStringItem("2", "Second item"), - list.NewStringItem("3", "Third item"), + list.NewStringItem("First item"), + list.NewStringItem("Second item"), + list.NewStringItem("Third item"), } // Create a list with options l := list.New(items...) l.SetSize(80, 10) - l.SetSelectedIndex(0) + l.SetSelected(0) if true { l.Focus() } @@ -109,7 +109,7 @@ func Example_focusable() { // Create list with first item selected and focused l := list.New(items...) l.SetSize(80, 20) - l.SetSelectedIndex(0) + l.SetSelected(0) if true { l.Focus() } @@ -127,8 +127,8 @@ func Example_focusable() { // Example demonstrates dynamic item updates. func Example_dynamicUpdates() { items := []list.Item{ - list.NewStringItem("1", "Item 1"), - list.NewStringItem("2", "Item 2"), + list.NewStringItem("Item 1"), + list.NewStringItem("Item 2"), } l := list.New(items...) @@ -140,13 +140,13 @@ func Example_dynamicUpdates() { l.Draw(&screen, area) // Update an item - l.UpdateItem("2", list.NewStringItem("2", "Updated Item 2")) + l.UpdateItem(2, list.NewStringItem("Updated Item 2")) // Draw again - only changed item is re-rendered l.Draw(&screen, area) // Append a new item - l.AppendItem(list.NewStringItem("3", "New Item 3")) + l.AppendItem(list.NewStringItem("New Item 3")) // Draw again - master buffer grows efficiently l.Draw(&screen, area) @@ -161,7 +161,6 @@ func Example_scrolling() { items := make([]list.Item, 100) for i := range items { items[i] = list.NewStringItem( - fmt.Sprintf("%d", i), fmt.Sprintf("Item %d", i), ) } @@ -169,7 +168,7 @@ func Example_scrolling() { // Create list with small viewport l := list.New(items...) l.SetSize(80, 10) - l.SetSelectedIndex(0) + l.SetSelected(0) // Draw initial view (shows items 0-9) screen := uv.NewScreenBuffer(80, 10) @@ -181,7 +180,7 @@ func Example_scrolling() { l.Draw(&screen, area) // Now shows items 5-14 // Jump to specific item - l.ScrollToItem("50") + l.ScrollToItem(50) l.Draw(&screen, area) // Now shows item 50 and neighbors // Scroll to bottom @@ -258,9 +257,9 @@ func Example_variableHeights() { func Example_markdown() { // Create markdown items items := []list.Item{ - list.NewMarkdownItem("1", "# Welcome\n\nThis is a **markdown** item."), - list.NewMarkdownItem("2", "## Features\n\n- Supports **bold**\n- Supports *italic*\n- Supports `code`"), - list.NewMarkdownItem("3", "### Code Block\n\n```go\nfunc main() {\n fmt.Println(\"Hello\")\n}\n```"), + list.NewMarkdownItem("# Welcome\n\nThis is a **markdown** item."), + list.NewMarkdownItem("## Features\n\n- Supports **bold**\n- Supports *italic*\n- Supports `code`"), + list.NewMarkdownItem("### Code Block\n\n```go\nfunc main() {\n fmt.Println(\"Hello\")\n}\n```"), } // Create list diff --git a/internal/ui/list/item.go b/internal/ui/list/item.go index 1b85cff42a18e1f12f801605b301778773510590..51f930d033d61d7f89634222f0f05b3e0041ac17 100644 --- a/internal/ui/list/item.go +++ b/internal/ui/list/item.go @@ -60,9 +60,6 @@ func toUVStyle(lgStyle lipgloss.Style) uv.Style { type Item interface { uv.Drawable - // ID returns unique identifier for this item. - ID() string - // Height returns the item's height in lines for the given width. // This allows items to calculate height based on text wrapping and available space. Height(width int) int @@ -272,7 +269,6 @@ func (b *BaseHighlightable) ApplyHighlight(buf *uv.ScreenBuffer, width, height i type StringItem struct { BaseFocusable BaseHighlightable - id string content string // Raw content string (may contain ANSI styles) wrap bool // Whether to wrap text @@ -305,9 +301,8 @@ func LipglossStyleToCellStyler(lgStyle lipgloss.Style) CellStyler { } // NewStringItem creates a new string item with the given ID and content. -func NewStringItem(id, content string) *StringItem { +func NewStringItem(content string) *StringItem { s := &StringItem{ - id: id, content: content, wrap: false, cache: make(map[int]string), @@ -317,9 +312,8 @@ func NewStringItem(id, content string) *StringItem { } // NewWrappingStringItem creates a new string item that wraps text to fit width. -func NewWrappingStringItem(id, content string) *StringItem { +func NewWrappingStringItem(content string) *StringItem { s := &StringItem{ - id: id, content: content, wrap: true, cache: make(map[int]string), @@ -335,11 +329,6 @@ func (s *StringItem) WithFocusStyles(focusStyle, blurStyle *lipgloss.Style) *Str return s } -// ID implements Item. -func (s *StringItem) ID() string { - return s.id -} - // Height implements Item. func (s *StringItem) Height(width int) int { // Calculate content width if we have styles @@ -423,7 +412,6 @@ func (s *StringItem) SetHighlight(startLine, startCol, endLine, endCol int) { type MarkdownItem struct { BaseFocusable BaseHighlightable - id string markdown string // Raw markdown content styleConfig *ansi.StyleConfig // Optional style configuration maxWidth int // Maximum wrap width (default 120) @@ -438,9 +426,8 @@ const DefaultMarkdownMaxWidth = 120 // NewMarkdownItem creates a new markdown item with the given ID and markdown content. // If focusStyle and blurStyle are both non-nil, the item will implement Focusable. -func NewMarkdownItem(id, markdown string) *MarkdownItem { +func NewMarkdownItem(markdown string) *MarkdownItem { m := &MarkdownItem{ - id: id, markdown: markdown, maxWidth: DefaultMarkdownMaxWidth, cache: make(map[int]string), @@ -468,11 +455,6 @@ func (m *MarkdownItem) WithFocusStyles(focusStyle, blurStyle *lipgloss.Style) *M return m } -// ID implements Item. -func (m *MarkdownItem) ID() string { - return m.id -} - // Height implements Item. func (m *MarkdownItem) Height(width int) int { // Render the markdown to get its height @@ -563,30 +545,23 @@ func (m *MarkdownItem) SetHighlight(startLine, startCol, endLine, endCol int) { } // Gap is a 1-line spacer item used to add gaps between items. -var Gap = NewSpacerItem("spacer-gap", 1) +var Gap = NewSpacerItem(1) // SpacerItem is an empty item that takes up vertical space. // Useful for adding gaps between items in a list. type SpacerItem struct { - id string height int } var _ Item = (*SpacerItem)(nil) // NewSpacerItem creates a new spacer item with the given ID and height in lines. -func NewSpacerItem(id string, height int) *SpacerItem { +func NewSpacerItem(height int) *SpacerItem { return &SpacerItem{ - id: id, height: height, } } -// ID implements Item. -func (s *SpacerItem) ID() string { - return s.id -} - // Height implements Item. func (s *SpacerItem) Height(width int) int { return s.height diff --git a/internal/ui/list/item_test.go b/internal/ui/list/item_test.go index 4ed2529441fcf2954436eacb0699224054ae4ee4..550a7b3a3cbe1bda035e91fca3efd01df87395db 100644 --- a/internal/ui/list/item_test.go +++ b/internal/ui/list/item_test.go @@ -10,9 +10,9 @@ import ( func TestRenderHelper(t *testing.T) { items := []Item{ - NewStringItem("1", "Item 1"), - NewStringItem("2", "Item 2"), - NewStringItem("3", "Item 3"), + NewStringItem("Item 1"), + NewStringItem("Item 2"), + NewStringItem("Item 3"), } l := New(items...) @@ -39,11 +39,11 @@ func TestRenderHelper(t *testing.T) { func TestRenderWithScrolling(t *testing.T) { items := []Item{ - NewStringItem("1", "Item 1"), - NewStringItem("2", "Item 2"), - NewStringItem("3", "Item 3"), - NewStringItem("4", "Item 4"), - NewStringItem("5", "Item 5"), + NewStringItem("Item 1"), + NewStringItem("Item 2"), + NewStringItem("Item 3"), + NewStringItem("Item 4"), + NewStringItem("Item 5"), } l := New(items...) @@ -89,8 +89,8 @@ func TestRenderEmptyList(t *testing.T) { func TestRenderVsDrawConsistency(t *testing.T) { items := []Item{ - NewStringItem("1", "Item 1"), - NewStringItem("2", "Item 2"), + NewStringItem("Item 1"), + NewStringItem("Item 2"), } l := New(items...) @@ -119,7 +119,7 @@ func TestRenderVsDrawConsistency(t *testing.T) { func BenchmarkRender(b *testing.B) { items := make([]Item, 100) for i := range items { - items[i] = NewStringItem(string(rune(i)), "Item content here") + items[i] = NewStringItem("Item content here") } l := New(items...) @@ -135,7 +135,7 @@ func BenchmarkRender(b *testing.B) { func BenchmarkRenderWithScrolling(b *testing.B) { items := make([]Item, 1000) for i := range items { - items[i] = NewStringItem(string(rune(i)), "Item content here") + items[i] = NewStringItem("Item content here") } l := New(items...) @@ -150,7 +150,7 @@ func BenchmarkRenderWithScrolling(b *testing.B) { } func TestStringItemCache(t *testing.T) { - item := NewStringItem("1", "Test content") + item := NewStringItem("Test content") // First draw at width 80 should populate cache screen1 := uv.NewScreenBuffer(80, 5) @@ -188,14 +188,14 @@ func TestStringItemCache(t *testing.T) { func TestWrappingItemHeight(t *testing.T) { // Short text that fits in one line - item1 := NewWrappingStringItem("1", "Short") + item1 := NewWrappingStringItem("Short") if h := item1.Height(80); h != 1 { t.Errorf("expected height 1 for short text, got %d", h) } // Long text that wraps longText := "This is a very long line that will definitely wrap when constrained to a narrow width" - item2 := NewWrappingStringItem("2", longText) + item2 := NewWrappingStringItem(longText) // At width 80, should be fewer lines than width 20 height80 := item2.Height(80) @@ -207,7 +207,7 @@ func TestWrappingItemHeight(t *testing.T) { } // Non-wrapping version should always be 1 line - item3 := NewStringItem("3", longText) + item3 := NewStringItem(longText) if h := item3.Height(20); h != 1 { t.Errorf("expected height 1 for non-wrapping item, got %d", h) } @@ -215,11 +215,7 @@ func TestWrappingItemHeight(t *testing.T) { func TestMarkdownItemBasic(t *testing.T) { markdown := "# Hello\n\nThis is a **test**." - item := NewMarkdownItem("1", markdown) - - if item.ID() != "1" { - t.Errorf("expected ID '1', got '%s'", item.ID()) - } + item := NewMarkdownItem(markdown) // Test that height is calculated height := item.Height(80) @@ -241,7 +237,7 @@ func TestMarkdownItemBasic(t *testing.T) { func TestMarkdownItemCache(t *testing.T) { markdown := "# Test\n\nSome content." - item := NewMarkdownItem("1", markdown) + item := NewMarkdownItem(markdown) // First render at width 80 should populate cache height1 := item.Height(80) @@ -267,7 +263,7 @@ func TestMarkdownItemCache(t *testing.T) { func TestMarkdownItemMaxCacheWidth(t *testing.T) { markdown := "# Test\n\nSome content." - item := NewMarkdownItem("1", markdown).WithMaxWidth(50) + item := NewMarkdownItem(markdown).WithMaxWidth(50) // Render at width 40 (below limit) - should cache at width 40 _ = item.Height(40) @@ -302,7 +298,7 @@ func TestMarkdownItemWithStyleConfig(t *testing.T) { }, } - item := NewMarkdownItem("1", markdown).WithStyleConfig(styleConfig) + item := NewMarkdownItem(markdown).WithStyleConfig(styleConfig) // Render should use the custom style height := item.Height(80) @@ -323,9 +319,9 @@ func TestMarkdownItemWithStyleConfig(t *testing.T) { func TestMarkdownItemInList(t *testing.T) { items := []Item{ - NewMarkdownItem("1", "# First\n\nMarkdown item."), - NewMarkdownItem("2", "# Second\n\nAnother item."), - NewStringItem("3", "Regular string item"), + NewMarkdownItem("# First\n\nMarkdown item."), + NewMarkdownItem("# Second\n\nAnother item."), + NewStringItem("Regular string item"), } l := New(items...) @@ -353,7 +349,7 @@ func TestMarkdownItemHeightWithWidth(t *testing.T) { // Test that widths are capped to maxWidth markdown := "This is a paragraph with some text." - item := NewMarkdownItem("1", markdown).WithMaxWidth(50) + item := NewMarkdownItem(markdown).WithMaxWidth(50) // At width 30 (below limit), should cache and render at width 30 height30 := item.Height(30) @@ -381,7 +377,7 @@ func TestMarkdownItemHeightWithWidth(t *testing.T) { func BenchmarkMarkdownItemRender(b *testing.B) { markdown := "# Heading\n\nThis is a paragraph with **bold** and *italic* text.\n\n- Item 1\n- Item 2\n- Item 3" - item := NewMarkdownItem("1", markdown) + item := NewMarkdownItem(markdown) // Prime the cache screen := uv.NewScreenBuffer(80, 10) @@ -401,7 +397,7 @@ func BenchmarkMarkdownItemUncached(b *testing.B) { b.ResetTimer() for i := 0; i < b.N; i++ { - item := NewMarkdownItem("1", markdown) + item := NewMarkdownItem(markdown) screen := uv.NewScreenBuffer(80, 10) area := uv.Rect(0, 0, 80, 10) item.Draw(&screen, area) @@ -409,12 +405,7 @@ func BenchmarkMarkdownItemUncached(b *testing.B) { } func TestSpacerItem(t *testing.T) { - spacer := NewSpacerItem("spacer1", 3) - - // Check ID - if spacer.ID() != "spacer1" { - t.Errorf("expected ID 'spacer1', got %q", spacer.ID()) - } + spacer := NewSpacerItem(3) // Check height if h := spacer.Height(80); h != 3 { @@ -444,11 +435,11 @@ func TestSpacerItem(t *testing.T) { func TestSpacerItemInList(t *testing.T) { // Create a list with items separated by spacers items := []Item{ - NewStringItem("1", "Item 1"), - NewSpacerItem("spacer1", 1), - NewStringItem("2", "Item 2"), - NewSpacerItem("spacer2", 2), - NewStringItem("3", "Item 3"), + NewStringItem("Item 1"), + NewSpacerItem(1), + NewStringItem("Item 2"), + NewSpacerItem(2), + NewStringItem("Item 3"), } l := New(items...) @@ -477,28 +468,28 @@ func TestSpacerItemInList(t *testing.T) { func TestSpacerItemNavigation(t *testing.T) { // Spacers should not be selectable (they're not focusable) items := []Item{ - NewStringItem("1", "Item 1"), - NewSpacerItem("spacer1", 1), - NewStringItem("2", "Item 2"), + NewStringItem("Item 1"), + NewSpacerItem(1), + NewStringItem("Item 2"), } l := New(items...) l.SetSize(20, 10) // Select first item - l.SetSelectedIndex(0) + l.SetSelected(0) if l.SelectedIndex() != 0 { t.Errorf("expected selected index 0, got %d", l.SelectedIndex()) } // Can select the spacer (it's a valid item, just not focusable) - l.SetSelectedIndex(1) + l.SetSelected(1) if l.SelectedIndex() != 1 { t.Errorf("expected selected index 1, got %d", l.SelectedIndex()) } // Can select item after spacer - l.SetSelectedIndex(2) + l.SetSelected(2) if l.SelectedIndex() != 2 { t.Errorf("expected selected index 2, got %d", l.SelectedIndex()) } @@ -512,11 +503,11 @@ func uintPtr(v uint) *uint { func TestListDoesNotEatLastLine(t *testing.T) { // Create items that exactly fill the viewport items := []Item{ - NewStringItem("1", "Line 1"), - NewStringItem("2", "Line 2"), - NewStringItem("3", "Line 3"), - NewStringItem("4", "Line 4"), - NewStringItem("5", "Line 5"), + NewStringItem("Line 1"), + NewStringItem("Line 2"), + NewStringItem("Line 3"), + NewStringItem("Line 4"), + NewStringItem("Line 5"), } // Create list with height exactly matching content (5 lines, no gaps) @@ -527,7 +518,7 @@ func TestListDoesNotEatLastLine(t *testing.T) { output := l.Render() // Count actual lines in output - lines := strings.Split(strings.TrimRight(output, "\r\n"), "\r\n") + lines := strings.Split(strings.TrimRight(output, "\n"), "\n") actualLineCount := 0 for _, line := range lines { if strings.TrimSpace(line) != "" { @@ -560,13 +551,13 @@ func TestListDoesNotEatLastLine(t *testing.T) { func TestListWithScrollDoesNotEatLastLine(t *testing.T) { // Create more items than viewport height items := []Item{ - NewStringItem("1", "Item 1"), - NewStringItem("2", "Item 2"), - NewStringItem("3", "Item 3"), - NewStringItem("4", "Item 4"), - NewStringItem("5", "Item 5"), - NewStringItem("6", "Item 6"), - NewStringItem("7", "Item 7"), + NewStringItem("Item 1"), + NewStringItem("Item 2"), + NewStringItem("Item 3"), + NewStringItem("Item 4"), + NewStringItem("Item 5"), + NewStringItem("Item 6"), + NewStringItem("Item 7"), } // Viewport shows 3 items at a time diff --git a/internal/ui/list/list.go b/internal/ui/list/list.go index 6fd116c5a106d5e26db58189e91d2e4b60da956d..7cde0f879ae3705421d5a6a27fd04c87a59ac0bc 100644 --- a/internal/ui/list/list.go +++ b/internal/ui/list/list.go @@ -16,8 +16,7 @@ type List struct { width, height int // Data - items []Item - indexMap map[string]int // ID -> index for fast lookup + items []Item // Focus & Selection focused bool @@ -28,23 +27,23 @@ type List struct { totalHeight int // Item positioning in master buffer - itemPositions map[string]itemPosition + itemPositions []itemPosition // Viewport state offset int // Scroll offset in lines from top // Mouse state mouseDown bool - mouseDownItem string // Item ID where mouse was pressed - mouseDownX int // X position in item content (character offset) - mouseDownY int // Y position in item (line offset) - mouseDragItem string // Current item being dragged over - mouseDragX int // Current X in item content - mouseDragY int // Current Y in item + mouseDownItem int // Item index where mouse was pressed + mouseDownX int // X position in item content (character offset) + mouseDownY int // Y position in item (line offset) + mouseDragItem int // Current item index being dragged over + mouseDragX int // Current X in item content + mouseDragY int // Current Y in item // Dirty tracking dirty bool - dirtyItems map[string]bool + dirtyItems map[int]bool } type itemPosition struct { @@ -56,15 +55,11 @@ type itemPosition struct { func New(items ...Item) *List { l := &List{ items: items, - indexMap: make(map[string]int, len(items)), - itemPositions: make(map[string]itemPosition, len(items)), - dirtyItems: make(map[string]bool), + itemPositions: make([]itemPosition, len(items)), + dirtyItems: make(map[int]bool), selectedIdx: -1, - } - - // Build index map - for i, item := range items { - l.indexMap[item.ID()] = i + mouseDownItem: -1, + mouseDragItem: -1, } l.dirty = true @@ -251,7 +246,7 @@ func (l *List) rebuildMasterBuffer() { // Draw each item currentY := 0 - for _, item := range l.items { + for i, item := range l.items { itemHeight := item.Height(l.width) // Draw item to master buffer @@ -259,7 +254,7 @@ func (l *List) rebuildMasterBuffer() { item.Draw(l.masterBuffer, area) // Store position - l.itemPositions[item.ID()] = itemPosition{ + l.itemPositions[i] = itemPosition{ startLine: currentY, height: itemHeight, } @@ -269,7 +264,7 @@ func (l *List) rebuildMasterBuffer() { } l.dirty = false - l.dirtyItems = make(map[string]bool) + l.dirtyItems = make(map[int]bool) } // updateDirtyItems efficiently updates only changed items using slice operations. @@ -280,21 +275,9 @@ func (l *List) updateDirtyItems() { // Check if all dirty items have unchanged heights allSameHeight := true - for id := range l.dirtyItems { - idx, ok := l.indexMap[id] - if !ok { - continue - } - + for idx := range l.dirtyItems { item := l.items[idx] - pos, ok := l.itemPositions[id] - if !ok { - l.dirty = true - l.dirtyItems = make(map[string]bool) - l.rebuildMasterBuffer() - return - } - + pos := l.itemPositions[idx] newHeight := item.Height(l.width) if newHeight != pos.height { allSameHeight = false @@ -305,10 +288,9 @@ func (l *List) updateDirtyItems() { // Optimization: If all dirty items have unchanged heights, re-render in place if allSameHeight { buf := l.masterBuffer.Buffer - for id := range l.dirtyItems { - idx := l.indexMap[id] + for idx := range l.dirtyItems { item := l.items[idx] - pos := l.itemPositions[id] + pos := l.itemPositions[idx] // Clear the item's area for y := pos.startLine; y < pos.startLine+pos.height && y < len(buf.Lines); y++ { @@ -320,23 +302,22 @@ func (l *List) updateDirtyItems() { item.Draw(l.masterBuffer, area) } - l.dirtyItems = make(map[string]bool) + l.dirtyItems = make(map[int]bool) return } // Height changed - full rebuild l.dirty = true - l.dirtyItems = make(map[string]bool) + l.dirtyItems = make(map[int]bool) l.rebuildMasterBuffer() } // updatePositionsBelow updates the startLine for all items below the given index. func (l *List) updatePositionsBelow(fromIdx int, delta int) { for i := fromIdx + 1; i < len(l.items); i++ { - item := l.items[i] - pos := l.itemPositions[item.ID()] + pos := l.itemPositions[i] pos.startLine += delta - l.itemPositions[item.ID()] = pos + l.itemPositions[i] = pos } } @@ -395,13 +376,7 @@ func (l *List) Len() int { // SetItems replaces all items in the list. func (l *List) SetItems(items []Item) { l.items = items - l.indexMap = make(map[string]int, len(items)) - l.itemPositions = make(map[string]itemPosition, len(items)) - - for i, item := range items { - l.indexMap[item.ID()] = i - } - + l.itemPositions = make([]itemPosition, len(items)) l.dirty = true } @@ -410,15 +385,15 @@ func (l *List) Items() []Item { return l.items } -// AppendItem adds an item to the end of the list. -func (l *List) AppendItem(item Item) { +// AppendItem adds an item to the end of the list. Returns true if successful. +func (l *List) AppendItem(item Item) bool { l.items = append(l.items, item) - l.indexMap[item.ID()] = len(l.items) - 1 + l.itemPositions = append(l.itemPositions, itemPosition{}) // If buffer not built yet, mark dirty for full rebuild if l.masterBuffer == nil || l.width <= 0 { l.dirty = true - return + return true } // Process any pending dirty items before modifying buffer structure @@ -442,23 +417,20 @@ func (l *List) AppendItem(item Item) { item.Draw(l.masterBuffer, area) // Update tracking - l.itemPositions[item.ID()] = itemPosition{ + l.itemPositions[len(l.items)-1] = itemPosition{ startLine: startLine, height: itemHeight, } l.totalHeight += itemHeight + + return true } -// PrependItem adds an item to the beginning of the list. -func (l *List) PrependItem(item Item) { +// PrependItem adds an item to the beginning of the list. Returns true if +// successful. +func (l *List) PrependItem(item Item) bool { l.items = append([]Item{item}, l.items...) - - // Rebuild index map (all indices shifted) - l.indexMap = make(map[string]int, len(l.items)) - for i, itm := range l.items { - l.indexMap[itm.ID()] = i - } - + l.itemPositions = append([]itemPosition{{}}, l.itemPositions...) if l.selectedIdx >= 0 { l.selectedIdx++ } @@ -466,7 +438,7 @@ func (l *List) PrependItem(item Item) { // If buffer not built yet, mark dirty for full rebuild if l.masterBuffer == nil || l.width <= 0 { l.dirty = true - return + return true } // Process any pending dirty items before modifying buffer structure @@ -492,42 +464,41 @@ func (l *List) PrependItem(item Item) { item.Draw(l.masterBuffer, area) // Update all positions (shift everything down) - for i := 1; i < len(l.items); i++ { - itm := l.items[i] - if pos, ok := l.itemPositions[itm.ID()]; ok { - pos.startLine += itemHeight - l.itemPositions[itm.ID()] = pos - } + for i := range l.itemPositions { + pos := l.itemPositions[i] + pos.startLine += itemHeight + l.itemPositions[i] = pos } - // Add position for new item - l.itemPositions[item.ID()] = itemPosition{ + // Add position for new item at start + l.itemPositions[0] = itemPosition{ startLine: 0, height: itemHeight, } + l.totalHeight += itemHeight + + return true } -// UpdateItem replaces an item with the same ID. -func (l *List) UpdateItem(id string, item Item) { - idx, ok := l.indexMap[id] - if !ok { - return +// UpdateItem replaces an item with the same index. Returns true if successful. +func (l *List) UpdateItem(idx int, item Item) bool { + if idx < 0 || idx >= len(l.items) { + return false } - l.items[idx] = item - l.dirtyItems[id] = true + l.dirtyItems[idx] = true + return true } -// DeleteItem removes an item by ID. -func (l *List) DeleteItem(id string) { - idx, ok := l.indexMap[id] - if !ok { - return +// DeleteItem removes an item by index. Returns true if successful. +func (l *List) DeleteItem(idx int) bool { + if idx < 0 || idx >= len(l.items) { + return false } // Get position before deleting - pos, hasPos := l.itemPositions[id] + pos := l.itemPositions[idx] // Process any pending dirty items before modifying buffer structure if len(l.dirtyItems) > 0 { @@ -535,13 +506,7 @@ func (l *List) DeleteItem(id string) { } l.items = append(l.items[:idx], l.items[idx+1:]...) - delete(l.indexMap, id) - delete(l.itemPositions, id) - - // Rebuild index map for items after deleted one - for i := idx; i < len(l.items); i++ { - l.indexMap[l.items[i].ID()] = i - } + l.itemPositions = append(l.itemPositions[:idx], l.itemPositions[idx+1:]...) // Adjust selection if l.selectedIdx == idx { @@ -557,9 +522,9 @@ func (l *List) DeleteItem(id string) { } // If buffer not built yet, mark dirty for full rebuild - if l.masterBuffer == nil || !hasPos { + if l.masterBuffer == nil { l.dirty = true - return + return true } // Efficient delete: remove lines from buffer @@ -575,6 +540,8 @@ func (l *List) DeleteItem(id string) { // Position data corrupt, rebuild l.dirty = true } + + return true } // Focus focuses the list and the selected item (if focusable). @@ -595,12 +562,10 @@ func (l *List) Focused() bool { } // SetSelected sets the selected item by ID. -func (l *List) SetSelected(id string) { - idx, ok := l.indexMap[id] - if !ok { +func (l *List) SetSelected(idx int) { + if idx < 0 || idx >= len(l.items) { return } - if l.selectedIdx == idx { return } @@ -613,33 +578,25 @@ func (l *List) SetSelected(id string) { if prevIdx >= 0 && prevIdx < len(l.items) { if f, ok := l.items[prevIdx].(Focusable); ok { f.Blur() - l.dirtyItems[l.items[prevIdx].ID()] = true + l.dirtyItems[prevIdx] = true } } if f, ok := l.items[idx].(Focusable); ok { f.Focus() - l.dirtyItems[l.items[idx].ID()] = true + l.dirtyItems[idx] = true } } } -// SetSelectedIndex sets the selected item by index. -func (l *List) SetSelectedIndex(idx int) { - if idx < 0 || idx >= len(l.items) { - return - } - l.SetSelected(l.items[idx].ID()) -} - // SelectFirst selects the first item in the list. func (l *List) SelectFirst() { - l.SetSelectedIndex(0) + l.SetSelected(0) } // SelectLast selects the last item in the list. func (l *List) SelectLast() { - l.SetSelectedIndex(len(l.items) - 1) + l.SetSelected(len(l.items) - 1) } // SelectNextWrap selects the next item in the list (wraps to beginning). @@ -679,7 +636,7 @@ func (l *List) selectNext(wrap bool) { } // Select and scroll to this item - l.SetSelected(l.items[nextIdx].ID()) + l.SetSelected(nextIdx) return } } @@ -721,7 +678,7 @@ func (l *List) selectPrev(wrap bool) { } // Select and scroll to this item - l.SetSelected(l.items[prevIdx].ID()) + l.SetSelected(prevIdx) return } } @@ -773,13 +730,9 @@ func (l *List) ScrollToBottom() { } // ScrollToItem scrolls to make the item with the given ID visible. -func (l *List) ScrollToItem(id string) { +func (l *List) ScrollToItem(idx int) { l.ensureBuilt() - pos, ok := l.itemPositions[id] - if !ok { - return - } - + pos := l.itemPositions[idx] itemStart := pos.startLine itemEnd := pos.startLine + pos.height viewStart := l.offset @@ -805,7 +758,7 @@ func (l *List) ScrollToSelected() { if l.selectedIdx < 0 || l.selectedIdx >= len(l.items) { return } - l.ScrollToItem(l.items[l.selectedIdx].ID()) + l.ScrollToItem(l.selectedIdx) } // Offset returns the current scroll offset. @@ -825,15 +778,12 @@ func (l *List) SelectFirstInView() { viewportStart := l.offset viewportEnd := l.offset + l.height - for i, item := range l.items { - pos, ok := l.itemPositions[item.ID()] - if !ok { - continue - } + for i := range l.items { + pos := l.itemPositions[i] // Check if item is fully within viewport bounds if pos.startLine >= viewportStart && (pos.startLine+pos.height) <= viewportEnd { - l.SetSelectedIndex(i) + l.SetSelected(i) return } } @@ -847,15 +797,11 @@ func (l *List) SelectLastInView() { viewportEnd := l.offset + l.height for i := len(l.items) - 1; i >= 0; i-- { - item := l.items[i] - pos, ok := l.itemPositions[item.ID()] - if !ok { - continue - } + pos := l.itemPositions[i] // Check if item is fully within viewport bounds if pos.startLine >= viewportStart && (pos.startLine+pos.height) <= viewportEnd { - l.SetSelectedIndex(i) + l.SetSelected(i) return } } @@ -868,11 +814,7 @@ func (l *List) SelectedItemInView() bool { } // Get selected item ID and position - item := l.items[l.selectedIdx] - pos, ok := l.itemPositions[item.ID()] - if !ok { - return false - } + pos := l.itemPositions[l.selectedIdx] // Check if item is within viewport bounds viewportStart := l.offset @@ -896,7 +838,7 @@ func (l *List) focusSelectedItem() { item := l.items[l.selectedIdx] if f, ok := item.(Focusable); ok { f.Focus() - l.dirtyItems[item.ID()] = true + l.dirtyItems[l.selectedIdx] = true } } @@ -909,7 +851,7 @@ func (l *List) blurSelectedItem() { item := l.items[l.selectedIdx] if f, ok := item.(Focusable); ok { f.Blur() - l.dirtyItems[item.ID()] = true + l.dirtyItems[l.selectedIdx] = true } } @@ -923,8 +865,8 @@ func (l *List) HandleMouseDown(x, y int) bool { bufferY := y + l.offset // Find which item was clicked - itemID, itemY := l.findItemAtPosition(bufferY) - if itemID == "" { + itemIdx, itemY := l.findItemAtPosition(bufferY) + if itemIdx < 0 { return false } @@ -933,17 +875,15 @@ func (l *List) HandleMouseDown(x, y int) bool { // Items can interpret this as character offset in their content l.mouseDown = true - l.mouseDownItem = itemID + l.mouseDownItem = itemIdx l.mouseDownX = x l.mouseDownY = itemY - l.mouseDragItem = itemID + l.mouseDragItem = itemIdx l.mouseDragX = x l.mouseDragY = itemY // Select the clicked item - if idx, ok := l.indexMap[itemID]; ok { - l.SetSelectedIndex(idx) - } + l.SetSelected(itemIdx) return true } @@ -962,12 +902,12 @@ func (l *List) HandleMouseDrag(x, y int) bool { bufferY := y + l.offset // Find which item we're dragging over - itemID, itemY := l.findItemAtPosition(bufferY) - if itemID == "" { + itemIdx, itemY := l.findItemAtPosition(bufferY) + if itemIdx < 0 { return false } - l.mouseDragItem = itemID + l.mouseDragItem = itemIdx l.mouseDragX = x l.mouseDragY = itemY @@ -994,47 +934,46 @@ func (l *List) HandleMouseUp(x, y int) bool { // ClearHighlight clears any active text highlighting. func (l *List) ClearHighlight() { - for _, item := range l.items { + for i, item := range l.items { if h, ok := item.(Highlightable); ok { h.SetHighlight(-1, -1, -1, -1) - l.dirtyItems[item.ID()] = true + l.dirtyItems[i] = true } } + l.mouseDownItem = -1 + l.mouseDragItem = -1 } // findItemAtPosition finds the item at the given master buffer y coordinate. -// Returns the item ID and the y offset within that item. -func (l *List) findItemAtPosition(bufferY int) (itemID string, itemY int) { +// Returns the item index and the y offset within that item. It returns -1, -1 +// if no item is found. +func (l *List) findItemAtPosition(bufferY int) (itemIdx int, itemY int) { if bufferY < 0 || bufferY >= l.totalHeight { - return "", 0 + return -1, -1 } // Linear search through items to find which one contains this y // This could be optimized with binary search if needed - for _, item := range l.items { - pos, ok := l.itemPositions[item.ID()] - if !ok { - continue - } - + for i := range l.items { + pos := l.itemPositions[i] if bufferY >= pos.startLine && bufferY < pos.startLine+pos.height { - return item.ID(), bufferY - pos.startLine + return i, bufferY - pos.startLine } } - return "", 0 + return -1, -1 } // updateHighlight updates the highlight range for highlightable items. // Supports highlighting across multiple items and respects drag direction. func (l *List) updateHighlight() { - if l.mouseDownItem == "" { + if l.mouseDownItem < 0 { return } // Get start and end item indices - downItemIdx := l.indexMap[l.mouseDownItem] - dragItemIdx := l.indexMap[l.mouseDragItem] + downItemIdx := l.mouseDownItem + dragItemIdx := l.mouseDragItem // Determine selection direction draggingDown := dragItemIdx > downItemIdx || @@ -1064,10 +1003,10 @@ func (l *List) updateHighlight() { } // Clear all highlights first - for _, item := range l.items { + for i, item := range l.items { if h, ok := item.(Highlightable); ok { h.SetHighlight(-1, -1, -1, -1) - l.dirtyItems[item.ID()] = true + l.dirtyItems[i] = true } } @@ -1083,18 +1022,18 @@ func (l *List) updateHighlight() { item.SetHighlight(startLine, startCol, endLine, endCol) } else if idx == startItemIdx { // First item - from start position to end of item - pos := l.itemPositions[l.items[idx].ID()] + pos := l.itemPositions[idx] item.SetHighlight(startLine, startCol, pos.height-1, 9999) // 9999 = end of line } else if idx == endItemIdx { // Last item - from start of item to end position item.SetHighlight(0, 0, endLine, endCol) } else { // Middle item - fully highlighted - pos := l.itemPositions[l.items[idx].ID()] + pos := l.itemPositions[idx] item.SetHighlight(0, 0, pos.height-1, 9999) } - l.dirtyItems[l.items[idx].ID()] = true + l.dirtyItems[idx] = true } } @@ -1110,7 +1049,7 @@ func (l *List) GetHighlightedText() string { var result strings.Builder // Iterate through items to find highlighted ones - for _, item := range l.items { + for i, item := range l.items { h, ok := item.(Highlightable) if !ok { continue @@ -1121,10 +1060,7 @@ func (l *List) GetHighlightedText() string { continue } - pos, ok := l.itemPositions[item.ID()] - if !ok { - continue - } + pos := l.itemPositions[i] // Extract text from highlighted region in master buffer for y := startLine; y <= endLine && y < pos.height; y++ { diff --git a/internal/ui/list/list_test.go b/internal/ui/list/list_test.go index e4f6dcff6714af0e55dc7397809235f70b386766..ac2a64e5e06879340ec8da93d273dfd53882e60e 100644 --- a/internal/ui/list/list_test.go +++ b/internal/ui/list/list_test.go @@ -11,9 +11,9 @@ import ( func TestNewList(t *testing.T) { items := []Item{ - NewStringItem("1", "Item 1"), - NewStringItem("2", "Item 2"), - NewStringItem("3", "Item 3"), + NewStringItem("Item 1"), + NewStringItem("Item 2"), + NewStringItem("Item 3"), } l := New(items...) @@ -30,9 +30,9 @@ func TestNewList(t *testing.T) { func TestListDraw(t *testing.T) { items := []Item{ - NewStringItem("1", "Item 1"), - NewStringItem("2", "Item 2"), - NewStringItem("3", "Item 3"), + NewStringItem("Item 1"), + NewStringItem("Item 2"), + NewStringItem("Item 3"), } l := New(items...) @@ -54,52 +54,44 @@ func TestListDraw(t *testing.T) { func TestListAppendItem(t *testing.T) { items := []Item{ - NewStringItem("1", "Item 1"), + NewStringItem("Item 1"), } l := New(items...) - l.AppendItem(NewStringItem("2", "Item 2")) + l.AppendItem(NewStringItem("Item 2")) if len(l.items) != 2 { t.Errorf("expected 2 items after append, got %d", len(l.items)) } - - if l.items[1].ID() != "2" { - t.Errorf("expected item ID '2', got '%s'", l.items[1].ID()) - } } func TestListDeleteItem(t *testing.T) { items := []Item{ - NewStringItem("1", "Item 1"), - NewStringItem("2", "Item 2"), - NewStringItem("3", "Item 3"), + NewStringItem("Item 1"), + NewStringItem("Item 2"), + NewStringItem("Item 3"), } l := New(items...) - l.DeleteItem("2") + l.DeleteItem(2) if len(l.items) != 2 { t.Errorf("expected 2 items after delete, got %d", len(l.items)) } - - if l.items[1].ID() != "3" { - t.Errorf("expected item ID '3', got '%s'", l.items[1].ID()) - } } func TestListUpdateItem(t *testing.T) { items := []Item{ - NewStringItem("1", "Item 1"), - NewStringItem("2", "Item 2"), + NewStringItem("Item 1"), + NewStringItem("Item 2"), } l := New(items...) l.SetSize(80, 10) // Update item - newItem := NewStringItem("2", "Updated Item 2") - l.UpdateItem("2", newItem) + newItem := NewStringItem("Updated Item 2") + l.UpdateItem(1, newItem) if l.items[1].(*StringItem).content != "Updated Item 2" { t.Errorf("expected updated content, got '%s'", l.items[1].(*StringItem).content) @@ -108,13 +100,13 @@ func TestListUpdateItem(t *testing.T) { func TestListSelection(t *testing.T) { items := []Item{ - NewStringItem("1", "Item 1"), - NewStringItem("2", "Item 2"), - NewStringItem("3", "Item 3"), + NewStringItem("Item 1"), + NewStringItem("Item 2"), + NewStringItem("Item 3"), } l := New(items...) - l.SetSelectedIndex(0) + l.SetSelected(0) if l.SelectedIndex() != 0 { t.Errorf("expected selected index 0, got %d", l.SelectedIndex()) @@ -133,11 +125,11 @@ func TestListSelection(t *testing.T) { func TestListScrolling(t *testing.T) { items := []Item{ - NewStringItem("1", "Item 1"), - NewStringItem("2", "Item 2"), - NewStringItem("3", "Item 3"), - NewStringItem("4", "Item 4"), - NewStringItem("5", "Item 5"), + NewStringItem("Item 1"), + NewStringItem("Item 2"), + NewStringItem("Item 3"), + NewStringItem("Item 4"), + NewStringItem("Item 5"), } l := New(items...) @@ -208,7 +200,7 @@ func TestListFocus(t *testing.T) { l := New(items...) l.SetSize(80, 10) - l.SetSelectedIndex(0) + l.SetSelected(0) // Focus the list l.Focus() @@ -256,12 +248,12 @@ func TestFocusNavigationAfterAppendingToViewportHeight(t *testing.T) { // Start with one item items := []Item{ - NewStringItem("1", "Item 1").WithFocusStyles(&focusStyle, &blurStyle), + NewStringItem("Item 1").WithFocusStyles(&focusStyle, &blurStyle), } l := New(items...) l.SetSize(20, 15) // 15 lines viewport height - l.SetSelectedIndex(0) + l.SetSelected(0) l.Focus() // Initial draw to build buffer @@ -271,12 +263,12 @@ func TestFocusNavigationAfterAppendingToViewportHeight(t *testing.T) { // Append items until we exceed viewport height // Each focusable item with border is 5 lines tall for i := 2; i <= 4; i++ { - item := NewStringItem(string(rune('0'+i)), "Item "+string(rune('0'+i))).WithFocusStyles(&focusStyle, &blurStyle) + item := NewStringItem("Item "+string(rune('0'+i))).WithFocusStyles(&focusStyle, &blurStyle) l.AppendItem(item) } // Select the last item - l.SetSelectedIndex(3) + l.SetSelected(3) // Draw screen = uv.NewScreenBuffer(20, 15) @@ -318,7 +310,7 @@ func TestFocusableItemUpdate(t *testing.T) { BorderForeground(lipgloss.Color("240")) // Create a focusable item - item := NewStringItem("1", "Test Item").WithFocusStyles(&focusStyle, &blurStyle) + item := NewStringItem("Test Item").WithFocusStyles(&focusStyle, &blurStyle) // Initially not focused - render with blur style screen1 := uv.NewScreenBuffer(20, 5) @@ -369,14 +361,14 @@ func TestFocusableItemHeightWithBorder(t *testing.T) { Border(lipgloss.RoundedBorder()) // Item without styles has height 1 - plainItem := NewStringItem("1", "Test") + plainItem := NewStringItem("Test") plainHeight := plainItem.Height(20) if plainHeight != 1 { t.Errorf("expected plain height 1, got %d", plainHeight) } // Item with border should add border height (2 lines) - item := NewStringItem("2", "Test").WithFocusStyles(&borderStyle, &borderStyle) + item := NewStringItem("Test").WithFocusStyles(&borderStyle, &borderStyle) itemHeight := item.Height(20) expectedHeight := 1 + 2 // content + border if itemHeight != expectedHeight { @@ -396,14 +388,14 @@ func TestFocusableItemInList(t *testing.T) { // Create list with focusable items items := []Item{ - NewStringItem("1", "Item 1").WithFocusStyles(&focusStyle, &blurStyle), - NewStringItem("2", "Item 2").WithFocusStyles(&focusStyle, &blurStyle), - NewStringItem("3", "Item 3").WithFocusStyles(&focusStyle, &blurStyle), + NewStringItem("Item 1").WithFocusStyles(&focusStyle, &blurStyle), + NewStringItem("Item 2").WithFocusStyles(&focusStyle, &blurStyle), + NewStringItem("Item 3").WithFocusStyles(&focusStyle, &blurStyle), } l := New(items...) l.SetSize(80, 20) - l.SetSelectedIndex(0) + l.SetSelected(0) // Focus the list l.Focus() @@ -421,7 +413,7 @@ func TestFocusableItemInList(t *testing.T) { } // Select second item - l.SetSelectedIndex(1) + l.SetSelected(1) // First item should be blurred, second focused if firstItem.IsFocused() { @@ -447,7 +439,7 @@ func TestFocusableItemInList(t *testing.T) { func TestFocusableItemWithNilStyles(t *testing.T) { // Test with nil styles - should render inner item directly - item := NewStringItem("1", "Plain Item").WithFocusStyles(nil, nil) + item := NewStringItem("Plain Item").WithFocusStyles(nil, nil) // Height should be based on content (no border since styles are nil) itemHeight := item.Height(20) @@ -488,7 +480,7 @@ func TestFocusableItemWithOnlyFocusStyle(t *testing.T) { Border(lipgloss.RoundedBorder()). BorderForeground(lipgloss.Color("86")) - item := NewStringItem("1", "Test").WithFocusStyles(&focusStyle, nil) + item := NewStringItem("Test").WithFocusStyles(&focusStyle, nil) // When not focused, should use nil blur style (no border) screen1 := uv.NewScreenBuffer(20, 5) @@ -519,15 +511,15 @@ func TestFocusableItemLastLineNotEaten(t *testing.T) { BorderForeground(lipgloss.Color("240")) items := []Item{ - NewStringItem("1", "Item 1").WithFocusStyles(&focusStyle, &blurStyle), + NewStringItem("Item 1").WithFocusStyles(&focusStyle, &blurStyle), Gap, - NewStringItem("2", "Item 2").WithFocusStyles(&focusStyle, &blurStyle), + NewStringItem("Item 2").WithFocusStyles(&focusStyle, &blurStyle), Gap, - NewStringItem("3", "Item 3").WithFocusStyles(&focusStyle, &blurStyle), + NewStringItem("Item 3").WithFocusStyles(&focusStyle, &blurStyle), Gap, - NewStringItem("4", "Item 4").WithFocusStyles(&focusStyle, &blurStyle), + NewStringItem("Item 4").WithFocusStyles(&focusStyle, &blurStyle), Gap, - NewStringItem("5", "Item 5").WithFocusStyles(&focusStyle, &blurStyle), + NewStringItem("Item 5").WithFocusStyles(&focusStyle, &blurStyle), } // Items with padding(1) and border are 5 lines each @@ -543,7 +535,7 @@ func TestFocusableItemLastLineNotEaten(t *testing.T) { l.Focus() // Select last item - l.SetSelectedIndex(len(items) - 1) + l.SetSelected(len(items) - 1) // Scroll to bottom l.ScrollToBottom() diff --git a/internal/ui/model/chat.go b/internal/ui/model/chat.go index 55fd9f58d161f1caa30ecb55b22e142d8e911aae..bc38a250876eab39eba7f1ffdba124741ee2ed5d 100644 --- a/internal/ui/model/chat.go +++ b/internal/ui/model/chat.go @@ -11,7 +11,6 @@ import ( "github.com/charmbracelet/crush/internal/ui/list" "github.com/charmbracelet/crush/internal/ui/styles" uv "github.com/charmbracelet/ultraviolet" - "github.com/google/uuid" ) // ChatAnimItem represents a chat animation item in the chat UI. @@ -57,20 +56,15 @@ func (c *ChatAnimItem) Height(int) int { return 1 } -// ID implements list.Item. -func (c *ChatAnimItem) ID() string { - return "anim" -} - // ChatNoContentItem represents a chat item with no content. type ChatNoContentItem struct { *list.StringItem } // NewChatNoContentItem creates a new instance of [ChatNoContentItem]. -func NewChatNoContentItem(t *styles.Styles, id string) *ChatNoContentItem { +func NewChatNoContentItem(t *styles.Styles) *ChatNoContentItem { c := new(ChatNoContentItem) - c.StringItem = list.NewStringItem(id, "No message content"). + c.StringItem = list.NewStringItem("No message content"). WithFocusStyles(&t.Chat.NoContentMessage, &t.Chat.NoContentMessage) return c } @@ -93,7 +87,7 @@ func NewChatMessageItem(t *styles.Styles, msg message.Message) *ChatMessageItem switch msg.Role { case message.User: - item := list.NewMarkdownItem(msg.ID, msg.Content().String()). + item := list.NewMarkdownItem(msg.Content().String()). WithFocusStyles(&t.Chat.UserMessageFocused, &t.Chat.UserMessageBlurred) item.SetHighlightStyle(list.LipglossStyleToCellStyler(t.TextSelection)) // TODO: Add attachments @@ -113,7 +107,7 @@ func NewChatMessageItem(t *styles.Styles, msg message.Message) *ChatMessageItem details := t.Chat.ErrorDetails.Render(finishedData.Details) errContent := fmt.Sprintf("%s %s\n\n%s", tag, title, details) - item := list.NewStringItem(msg.ID, errContent). + item := list.NewStringItem(errContent). WithFocusStyles(&t.Chat.AssistantMessageFocused, &t.Chat.AssistantMessageBlurred) c.item = item @@ -141,7 +135,7 @@ func NewChatMessageItem(t *styles.Styles, msg message.Message) *ChatMessageItem parts = append(parts, content) } - item := list.NewMarkdownItem(msg.ID, strings.Join(parts, "\n")). + item := list.NewMarkdownItem(strings.Join(parts, "\n")). WithFocusStyles(&t.Chat.AssistantMessageFocused, &t.Chat.AssistantMessageBlurred) item.SetHighlightStyle(list.LipglossStyleToCellStyler(t.TextSelection)) @@ -161,11 +155,6 @@ func (c *ChatMessageItem) Height(width int) int { return c.item.Height(width) } -// ID implements list.Item. -func (c *ChatMessageItem) ID() string { - return c.item.ID() -} - // Blur implements list.Focusable. func (c *ChatMessageItem) Blur() { if blurable, ok := c.item.(list.Focusable); ok { @@ -248,7 +237,7 @@ func (m *Chat) PrependItem(item list.Item) { // AppendMessage appends a new message item to the chat list. func (m *Chat) AppendMessage(msg message.Message) { if msg.ID == "" { - m.AppendItem(NewChatNoContentItem(m.com.Styles, uuid.NewString())) + m.AppendItem(NewChatNoContentItem(m.com.Styles)) } else { m.AppendItem(NewChatMessageItem(m.com.Styles, msg)) } @@ -258,7 +247,7 @@ func (m *Chat) AppendMessage(msg message.Message) { func (m *Chat) AppendItem(item list.Item) { if m.Len() > 0 { // Always add a spacer between messages - m.list.AppendItem(list.NewSpacerItem(uuid.NewString(), 1)) + m.list.AppendItem(list.NewSpacerItem(1)) } m.list.AppendItem(item) } @@ -298,9 +287,9 @@ func (m *Chat) SelectedItemInView() bool { return m.list.SelectedItemInView() } -// SetSelectedIndex sets the selected message index in the chat list. -func (m *Chat) SetSelectedIndex(index int) { - m.list.SetSelectedIndex(index) +// SetSelected sets the selected message index in the chat list. +func (m *Chat) SetSelected(index int) { + m.list.SetSelected(index) } // SelectPrev selects the previous message in the chat list. diff --git a/internal/ui/model/ui.go b/internal/ui/model/ui.go index a86642a86359f9fdcec621081162501bbd0588f6..50521afc211d17435ae03370e2c3008e8f74b196 100644 --- a/internal/ui/model/ui.go +++ b/internal/ui/model/ui.go @@ -326,7 +326,7 @@ func (m *UI) handleKeyPressMsg(msg tea.KeyPressMsg) (cmds []tea.Cmd) { m.focus = uiFocusMain m.textarea.Blur() m.chat.Focus() - m.chat.SetSelectedIndex(m.chat.Len() - 1) + m.chat.SetSelected(m.chat.Len() - 1) } case key.Matches(msg, m.keyMap.Chat.Up): m.chat.ScrollBy(-1)