@@ -9,7 +9,6 @@ import (
9 "github.com/charmbracelet/bubbles/v2/key"
9 "github.com/charmbracelet/bubbles/v2/key"
10 tea "github.com/charmbracelet/bubbletea/v2"
10 tea "github.com/charmbracelet/bubbletea/v2"
11 "github.com/charmbracelet/lipgloss/v2"
11 "github.com/charmbracelet/lipgloss/v2"
12− "github.com/opencode-ai/opencode/internal/logging"
13 "github.com/opencode-ai/opencode/internal/tui/components/anim"
12 "github.com/opencode-ai/opencode/internal/tui/components/anim"
14 "github.com/opencode-ai/opencode/internal/tui/layout"
13 "github.com/opencode-ai/opencode/internal/tui/layout"
15 "github.com/opencode-ai/opencode/internal/tui/util"
14 "github.com/opencode-ai/opencode/internal/tui/util"
@@ -39,7 +38,7 @@ type renderedItem struct {
40type model struct {
39type model struct {
41 width, height, offset int
40 width, height, offset int
42− finalHight int // this gets set when the last item is rendered to mark the max offset
41+ finalHeight int // this gets set when the last item is rendered to mark the max offset
43 reverse bool
42 reverse bool
44 help help.Model
43 help help.Model
45 keymap KeyMap
44 keymap KeyMap
@@ -95,7 +94,7 @@ func New(opts ...listOptions) ListModel {
95 gapSize: 0,
94 gapSize: 0,
96 padding: []int{},
95 padding: []int{},
97 selectedItemInx: -1,
96 selectedItemInx: -1,
98− finalHight: -1,
97+ finalHeight: -1,
99 lastRenderedInx: -1,
98 lastRenderedInx: -1,
100 renderedItems: new(sync.Map),
99 renderedItems: new(sync.Map),
@@ -160,20 +159,7 @@ func (m *model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
160 m.goToBottom()
159 m.goToBottom()
161 return m, nil
160 return m, nil
163− case anim.ColorCycleMsg:
162+ case anim.ColorCycleMsg, anim.StepCharsMsg:
164− logging.Info("ColorCycleMsg", "msg", msg)
165− for inx, item := range m.items {
166− if i, ok := item.(HasAnim); ok {
168− updated, cmd := i.Update(msg)
169− cmds = append(cmds, cmd)
170− m.UpdateItem(inx, updated.(util.Model))
174− return m, tea.Batch(cmds...)
175− case anim.StepCharsMsg:
176− logging.Info("ColorCycleMsg", "msg", msg)
177 for inx, item := range m.items {
163 for inx, item := range m.items {
178 if i, ok := item.(HasAnim); ok {
164 if i, ok := item.(HasAnim); ok {
179 if i.Spinning() {
165 if i.Spinning() {
@@ -189,7 +175,6 @@ func (m *model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
189 u, cmd := m.items[m.selectedItemInx].Update(msg)
175 u, cmd := m.items[m.selectedItemInx].Update(msg)
190 cmds = append(cmds, cmd)
176 cmds = append(cmds, cmd)
191 m.UpdateItem(m.selectedItemInx, u.(util.Model))
177 m.UpdateItem(m.selectedItemInx, u.(util.Model))
192− cmds = append(cmds, cmd)
193 return m, tea.Batch(cmds...)
178 return m, tea.Batch(cmds...)
@@ -232,7 +217,7 @@ func (m *model) renderVisibleReverse() {
232 itemLines = cachedContent.(renderedItem).lines
217 itemLines = cachedContent.(renderedItem).lines
234 itemLines = strings.Split(items[i].View(), "\n")
219 itemLines = strings.Split(items[i].View(), "\n")
235− if m.gapSize > 0 && realIndex != len(m.items)-1 {
220+ if m.gapSize > 0 {
236 for range m.gapSize {
221 for range m.gapSize {
237 itemLines = append(itemLines, "")
222 itemLines = append(itemLines, "")
@@ -245,7 +230,7 @@ func (m *model) renderVisibleReverse() {
247 if realIndex == 0 {
232 if realIndex == 0 {
248− m.finalHight = max(0, start+len(itemLines)-m.listHeight())
233+ m.finalHeight = max(0, start+len(itemLines)-m.listHeight())
250 m.renderedLines = append(itemLines, m.renderedLines...)
235 m.renderedLines = append(itemLines, m.renderedLines...)
251 m.lastRenderedInx = realIndex
236 m.lastRenderedInx = realIndex
@@ -256,9 +241,9 @@ func (m *model) renderVisibleReverse() {
256 start += len(itemLines)
241 start += len(itemLines)
258 m.needsRerender = false
243 m.needsRerender = false
259− if m.finalHight > -1 {
244+ if m.finalHeight > -1 {
260 // make sure we don't go over the final height, this can happen if we did not render the last item and we overshot the offset
245 // make sure we don't go over the final height, this can happen if we did not render the last item and we overshot the offset
261− m.offset = min(m.offset, m.finalHight)
246+ m.offset = min(m.offset, m.finalHeight)
263 maxHeight := min(m.listHeight(), len(m.renderedLines))
248 maxHeight := min(m.listHeight(), len(m.renderedLines))
264 if m.offset < len(m.renderedLines) {
249 if m.offset < len(m.renderedLines) {
@@ -293,7 +278,7 @@ func (m *model) renderVisible() {
293 itemLines = cachedContent.(renderedItem).lines
278 itemLines = cachedContent.(renderedItem).lines
295 itemLines = strings.Split(item.View(), "\n")
280 itemLines = strings.Split(item.View(), "\n")
296− if m.gapSize > 0 && realIndex != len(m.items)-1 {
281+ if m.gapSize > 0 {
297 for range m.gapSize {
282 for range m.gapSize {
298 itemLines = append(itemLines, "")
283 itemLines = append(itemLines, "")
@@ -310,7 +295,7 @@ func (m *model) renderVisible() {
312 if realIndex == len(m.items)-1 {
297 if realIndex == len(m.items)-1 {
313− m.finalHight = max(0, start+len(itemLines)-m.listHeight())
298+ m.finalHeight = max(0, start+len(itemLines)-m.listHeight())
316 m.renderedLines = append(m.renderedLines, itemLines...)
301 m.renderedLines = append(m.renderedLines, itemLines...)
@@ -319,9 +304,9 @@ func (m *model) renderVisible() {
320 m.needsRerender = false
305 m.needsRerender = false
321 maxHeight := min(m.listHeight(), len(m.renderedLines))
306 maxHeight := min(m.listHeight(), len(m.renderedLines))
322− if m.finalHight > -1 {
307+ if m.finalHeight > -1 {
323 // make sure we don't go over the final height, this can happen if we did not render the last item and we overshot the offset
308 // make sure we don't go over the final height, this can happen if we did not render the last item and we overshot the offset
324− m.offset = min(m.offset, m.finalHight)
309+ m.offset = min(m.offset, m.finalHeight)
326 if m.offset < len(m.renderedLines) {
311 if m.offset < len(m.renderedLines) {
327 m.content = strings.Join(m.renderedLines[m.offset:maxHeight+m.offset], "\n")
312 m.content = strings.Join(m.renderedLines[m.offset:maxHeight+m.offset], "\n")
@@ -412,6 +397,9 @@ func (m *model) downOneItem() tea.Cmd {
414func (m *model) goToBottom() tea.Cmd {
399func (m *model) goToBottom() tea.Cmd {
400+ if len(m.items) == 0 {
415 var cmds []tea.Cmd
403 var cmds []tea.Cmd
416 m.reverse = true
404 m.reverse = true
417 cmd := m.blurSelected()
405 cmd := m.blurSelected()
@@ -428,11 +416,14 @@ func (m *model) ResetView() {
428 m.renderedLines = []string{}
416 m.renderedLines = []string{}
429 m.offset = 0
417 m.offset = 0
430 m.lastRenderedInx = -1
418 m.lastRenderedInx = -1
431− m.finalHight = -1
419+ m.finalHeight = -1
432 m.needsRerender = true
420 m.needsRerender = true
435func (m *model) goToTop() tea.Cmd {
423func (m *model) goToTop() tea.Cmd {
424+ if len(m.items) == 0 {
436 var cmds []tea.Cmd
427 var cmds []tea.Cmd
437 m.reverse = false
428 m.reverse = false
438 cmd := m.blurSelected()
429 cmd := m.blurSelected()
@@ -480,7 +471,7 @@ func (m *model) rerenderItem(inx int) {
481 rerenderedItem := m.items[inx].View()
472 rerenderedItem := m.items[inx].View()
482 rerenderedLines := strings.Split(rerenderedItem, "\n")
473 rerenderedLines := strings.Split(rerenderedItem, "\n")
483− if m.gapSize > 0 && inx != len(m.items)-1 {
474+ if m.gapSize > 0 {
484 for range m.gapSize {
475 for range m.gapSize {
485 rerenderedLines = append(rerenderedLines, "")
476 rerenderedLines = append(rerenderedLines, "")
@@ -492,7 +483,6 @@ func (m *model) rerenderItem(inx int) {
493 // check if the item is in the content
484 // check if the item is in the content
494 start := cachedItem.start
485 start := cachedItem.start
495− logging.Info("rerenderItem", "inx", inx, "start", start, "cachedItem.start", cachedItem.start, "cachedItem.height", cachedItem.height)
496 end := start + cachedItem.height
486 end := start + cachedItem.height
497 totalLines := len(m.renderedLines)
487 totalLines := len(m.renderedLines)
498 if m.reverse {
488 if m.reverse {
@@ -504,9 +494,35 @@ func (m *model) rerenderItem(inx int) {
504 m.renderedLines = slices.Insert(m.renderedLines, start, rerenderedLines...)
494 m.renderedLines = slices.Insert(m.renderedLines, start, rerenderedLines...)
506 // TODO: if hight changed do something
496 // TODO: if hight changed do something
507− if cachedItem.height != len(rerenderedLines) && inx != len(m.items)-1 {
497+ if cachedItem.height != len(rerenderedLines) {
508 if inx == len(m.items)-1 {
498 if inx == len(m.items)-1 {
509− m.finalHight = max(0, start+len(rerenderedLines)-m.listHeight())
499+ m.finalHeight = max(0, start+len(rerenderedLines)-m.listHeight())
502+ // update the start of the other cached items
503+ currentStart := cachedItem.start + len(rerenderedLines)
505+ for i := inx - 1; i < len(m.items); i-- {
506+ if existing, ok := m.renderedItems.Load(i); ok {
507+ cached := existing.(renderedItem)
508+ cached.start = currentStart
509+ currentStart += cached.height
510+ m.renderedItems.Store(i, cached)
516+ for i := inx + 1; i < len(m.items); i++ {
517+ if existing, ok := m.renderedItems.Load(i); ok {
518+ cached := existing.(renderedItem)
519+ cached.start = currentStart
520+ currentStart += cached.height
521+ m.renderedItems.Store(i, cached)
512 m.renderedItems.Store(inx, renderedItem{
528 m.renderedItems.Store(inx, renderedItem{
@@ -518,11 +534,11 @@ func (m *model) rerenderItem(inx int) {
520func (m *model) increaseOffset(n int) {
536func (m *model) increaseOffset(n int) {
521− if m.finalHight > -1 {
537+ if m.finalHeight > -1 {
522− if m.offset < m.finalHight {
538+ if m.offset < m.finalHeight {
523 m.offset += n
539 m.offset += n
524− if m.offset > m.finalHight {
540+ if m.offset > m.finalHeight {
525− m.offset = m.finalHight
541+ m.offset = m.finalHeight
527 m.needsRerender = true
543 m.needsRerender = true
@@ -550,7 +566,8 @@ func (m *model) UpdateItem(inx int, item util.Model) {
550 i.Focus()
566 i.Focus()
553− m.ResetView()
569+ m.setItemSize(inx)
554 m.needsRerender = true
571 m.needsRerender = true
@@ -565,16 +582,15 @@ func (m *model) SetSize(width int, height int) tea.Cmd {
565 return nil
582 return nil
567 if m.height != height {
584 if m.height != height {
568− m.finalHight = -1
585+ m.finalHeight = -1
569 m.height = height
586 m.height = height
571 m.width = width
588 m.width = width
572 m.ResetView()
589 m.ResetView()
573− return m.setItemsSize()
590+ return m.setAllItemsSize()
576−func (m *model) setItemsSize() tea.Cmd {
593+func (m *model) getItemSize() int {
578 width := m.width
594 width := m.width
579 if m.padding != nil {
595 if m.padding != nil {
580 if len(m.padding) == 1 {
596 if len(m.padding) == 1 {
@@ -585,11 +601,22 @@ func (m *model) setItemsSize() tea.Cmd {
585 width -= m.padding[1] + m.padding[3]
601 width -= m.padding[1] + m.padding[3]
588− for _, item := range m.items {
604+ return width
607+func (m *model) setItemSize(inx int) tea.Cmd {
589− if i, ok := item.(layout.Sizeable); ok {
608+ if i, ok := m.items[inx].(layout.Sizeable); ok {
590− cmd := i.SetSize(width, 0) // height is not limited
609+ cmd := i.SetSize(m.getItemSize(), 0) // height is not limited
615+func (m *model) setAllItemsSize() tea.Cmd {
617+ for i := range m.items {
618+ cmd := m.setItemSize(i)
591− cmds = append(cmds, cmd)
619+ cmds = append(cmds, cmd)
594 return tea.Batch(cmds...)
621 return tea.Batch(cmds...)
@@ -612,11 +639,16 @@ func (m *model) listHeight() int {
613// AppendItem implements List.
640// AppendItem implements List.
614func (m *model) AppendItem(item util.Model) tea.Cmd {
641func (m *model) AppendItem(item util.Model) tea.Cmd {
615 cmd := item.Init()
643 cmd := item.Init()
644+ cmds = append(cmds, cmd)
616 m.items = append(m.items, item)
645 m.items = append(m.items, item)
646+ cmd = m.setItemSize(len(m.items) - 1)
647+ cmds = append(cmds, cmd)
617− m.goToBottom()
648+ cmd = m.goToBottom()
649+ cmds = append(cmds, cmd)
618 m.needsRerender = true
650 m.needsRerender = true
619− return cmd
651+ return tea.Batch(cmds...)
622// DeleteItem implements List.
654// DeleteItem implements List.
@@ -632,7 +664,9 @@ func (m *model) DeleteItem(i int) {
633// PrependItem implements List.
665// PrependItem implements List.
634func (m *model) PrependItem(item util.Model) tea.Cmd {
666func (m *model) PrependItem(item util.Model) tea.Cmd {
635 cmd := item.Init()
668 cmd := item.Init()
669+ cmds = append(cmds, cmd)
636 m.items = append([]util.Model{item}, m.items...)
670 m.items = append([]util.Model{item}, m.items...)
637 // update the indices of the rendered items
671 // update the indices of the rendered items
638 newRenderedItems := make(map[int]renderedItem)
672 newRenderedItems := make(map[int]renderedItem)
@@ -647,9 +681,12 @@ func (m *model) PrependItem(item util.Model) tea.Cmd {
647 for k, v := range newRenderedItems {
681 for k, v := range newRenderedItems {
648 m.renderedItems.Store(k, v)
682 m.renderedItems.Store(k, v)
650− m.goToTop()
684+ cmd = m.goToTop()
685+ cmds = append(cmds, cmd)
686+ cmd = m.setItemSize(0)
687+ cmds = append(cmds, cmd)
651 m.needsRerender = true
688 m.needsRerender = true
652− return cmd
689+ return tea.Batch(cmds...)
655func (m *model) setReverse(reverse bool) {
692func (m *model) setReverse(reverse bool) {
@@ -664,7 +701,7 @@ func (m *model) setReverse(reverse bool) {
664func (m *model) SetItems(items []util.Model) tea.Cmd {
701func (m *model) SetItems(items []util.Model) tea.Cmd {
665 m.items = items
702 m.items = items
666 var cmds []tea.Cmd
703 var cmds []tea.Cmd
667− cmd := m.setItemsSize()
704+ cmd := m.setAllItemsSize()
668 cmds = append(cmds, cmd)
705 cmds = append(cmds, cmd)
669 for _, item := range m.items {
706 for _, item := range m.items {
670 cmds = append(cmds, item.Init())
707 cmds = append(cmds, item.Init())
@@ -678,7 +715,6 @@ func (m *model) SetItems(items []util.Model) tea.Cmd {
678 cmd := m.focusSelected()
715 cmd := m.focusSelected()
679 cmds = append(cmds, cmd)
716 cmds = append(cmds, cmd)
681− m.needsRerender = true
682 m.ResetView()
718 m.ResetView()
683 return tea.Batch(cmds...)
719 return tea.Batch(cmds...)