on main help sign in
chore: fix viewport offset
6b3fe6aKujtim Hoxha
crush.json+9
@@ -3,5 +3,14 @@
3 "Go": {
3 "Go": {
4 "command": "gopls"
4 "command": "gopls"
5 }
5 }
6+ },
7+ "mcp": {
8+ "linear": {
9+ "type": "stdio",
10+ "command": "mcp-remote",
11+ "args": [
12+ "https://mcp.linear.app/sse"
13+ ]
14+ }
6 }
15 }
7}
16}
internal/tui/components/chat/chat.go+7−1
@@ -40,6 +40,7 @@ type MessageListCmp interface {
40 layout.Help
40 layout.Help
41
41
42 SetSession(session.Session) tea.Cmd
42 SetSession(session.Session) tea.Cmd
43+ GoToBottom() tea.Cmd
43}
44}
44
45
45// messageListCmp implements MessageListCmp, providing a virtualized list
46// messageListCmp implements MessageListCmp, providing a virtualized list
@@ -64,6 +65,7 @@ func New(app *app.App) MessageListCmp {
64 []list.Item{},
65 []list.Item{},
65 list.WithGap(1),
66 list.WithGap(1),
66 list.WithDirectionBackward(),
67 list.WithDirectionBackward(),
68+ list.WithFocus(false),
67 list.WithKeyMap(defaultListKeyMap),
69 list.WithKeyMap(defaultListKeyMap),
68 )
70 )
69 return &messageListCmp{
71 return &messageListCmp{
@@ -76,7 +78,7 @@ func New(app *app.App) MessageListCmp {
76
78
77// Init initializes the component.
79// Init initializes the component.
78func (m *messageListCmp) Init() tea.Cmd {
80func (m *messageListCmp) Init() tea.Cmd {
79− return tea.Sequence(m.listCmp.Init(), m.listCmp.Blur())
81+ return m.listCmp.Init()
80}
82}
81
83
82// Update handles incoming messages and updates the component state.
84// Update handles incoming messages and updates the component state.
@@ -531,3 +533,7 @@ func (m *messageListCmp) IsFocused() bool {
531func (m *messageListCmp) Bindings() []key.Binding {
533func (m *messageListCmp) Bindings() []key.Binding {
532 return m.defaultListKeyMap.KeyBindings()
534 return m.defaultListKeyMap.KeyBindings()
533}
535}
536+
537+func (m *messageListCmp) GoToBottom() tea.Cmd {
538+ return m.listCmp.GoToBottom()
539+}
internal/tui/exp/list/list.go+157−9
@@ -1,10 +1,12 @@
1package list
1package list
2
2
3import (
3import (
4+ "slices"
4 "strings"
5 "strings"
5
6
6 "github.com/charmbracelet/bubbles/v2/key"
7 "github.com/charmbracelet/bubbles/v2/key"
7 tea "github.com/charmbracelet/bubbletea/v2"
8 tea "github.com/charmbracelet/bubbletea/v2"
9+ "github.com/charmbracelet/crush/internal/tui/components/anim"
8 "github.com/charmbracelet/crush/internal/tui/components/core/layout"
10 "github.com/charmbracelet/crush/internal/tui/components/core/layout"
9 "github.com/charmbracelet/crush/internal/tui/util"
11 "github.com/charmbracelet/crush/internal/tui/util"
10 "github.com/charmbracelet/lipgloss/v2"
12 "github.com/charmbracelet/lipgloss/v2"
@@ -16,6 +18,10 @@ type Item interface {
16 ID() string
18 ID() string
17}
19}
18
20
21+type HasAnim interface {
22+ Item
23+ Spinning() bool
24+}
19type (
25type (
20 renderedMsg struct{}
26 renderedMsg struct{}
21 List[T Item] interface {
27 List[T Item] interface {
@@ -172,6 +178,18 @@ func (l *list[T]) Init() tea.Cmd {
172// Update implements List.
178// Update implements List.
173func (l *list[T]) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
179func (l *list[T]) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
174 switch msg := msg.(type) {
180 switch msg := msg.(type) {
181+ case anim.StepMsg:
182+ var cmds []tea.Cmd
183+ for _, item := range l.items {
184+ if i, ok := any(item).(HasAnim); ok && i.Spinning() {
185+ updated, cmd := i.Update(msg)
186+ cmds = append(cmds, cmd)
187+ if u, ok := updated.(T); ok {
188+ cmds = append(cmds, l.UpdateItem(u.ID(), u))
189+ }
190+ }
191+ }
192+ return l, tea.Batch(cmds...)
175 case tea.KeyPressMsg:
193 case tea.KeyPressMsg:
176 if l.focused {
194 if l.focused {
177 switch {
195 switch {
@@ -553,8 +571,44 @@ func (l *list[T]) renderItem(item Item) renderedItem {
553}
571}
554
572
555// AppendItem implements List.
573// AppendItem implements List.
556−func (l *list[T]) AppendItem(T) tea.Cmd {
574+func (l *list[T]) AppendItem(item T) tea.Cmd {
557− panic("unimplemented")
575+ var cmds []tea.Cmd
576+ cmd := item.Init()
577+ if cmd != nil {
578+ cmds = append(cmds, cmd)
579+ }
580+
581+ l.items = append(l.items, item)
582+ l.indexMap = make(map[string]int)
583+ for inx, item := range l.items {
584+ l.indexMap[item.ID()] = inx
585+ }
586+ if l.width > 0 && l.height > 0 {
587+ cmd = item.SetSize(l.width, l.height)
588+ if cmd != nil {
589+ cmds = append(cmds, cmd)
590+ }
591+ }
592+ cmd = l.render()
593+ if cmd != nil {
594+ cmds = append(cmds, cmd)
595+ }
596+ if l.direction == DirectionBackward {
597+ if l.offset == 0 {
598+ cmd = l.GoToBottom()
599+ if cmd != nil {
600+ cmds = append(cmds, cmd)
601+ }
602+ } else {
603+ newItem := l.renderedItems[item.ID()]
604+ newLines := newItem.height
605+ if len(l.items) > 1 {
606+ newLines += l.gap
607+ }
608+ l.offset = min(lipgloss.Height(l.rendered)-1, l.offset+newLines)
609+ }
610+ }
611+ return tea.Sequence(cmds...)
558}
612}
559
613
560// Blur implements List.
614// Blur implements List.
@@ -564,8 +618,34 @@ func (l *list[T]) Blur() tea.Cmd {
564}
618}
565
619
566// DeleteItem implements List.
620// DeleteItem implements List.
567−func (l *list[T]) DeleteItem(string) tea.Cmd {
621+func (l *list[T]) DeleteItem(id string) tea.Cmd {
568− panic("unimplemented")
622+ inx := l.indexMap[id]
623+ l.items = slices.Delete(l.items, inx, inx+1)
624+ delete(l.renderedItems, id)
625+ for inx, item := range l.items {
626+ l.indexMap[item.ID()] = inx
627+ }
628+
629+ if l.selectedItem == id {
630+ if inx > 0 {
631+ l.selectedItem = l.items[inx-1].ID()
632+ } else {
633+ l.selectedItem = ""
634+ }
635+ }
636+ cmd := l.render()
637+ if l.rendered != "" {
638+ renderedHeight := lipgloss.Height(l.rendered)
639+ if renderedHeight <= l.height {
640+ l.offset = 0
641+ } else {
642+ maxOffset := renderedHeight - l.height
643+ if l.offset > maxOffset {
644+ l.offset = maxOffset
645+ }
646+ }
647+ }
648+ return cmd
569}
649}
570
650
571// Focus implements List.
651// Focus implements List.
@@ -651,8 +731,35 @@ func (l *list[T]) MoveUp(n int) tea.Cmd {
651}
731}
652
732
653// PrependItem implements List.
733// PrependItem implements List.
654−func (l *list[T]) PrependItem(T) tea.Cmd {
734+func (l *list[T]) PrependItem(item T) tea.Cmd {
655− panic("unimplemented")
735+ cmds := []tea.Cmd{
736+ item.Init(),
737+ }
738+ l.items = append([]T{item}, l.items...)
739+ l.indexMap = make(map[string]int)
740+ for inx, item := range l.items {
741+ l.indexMap[item.ID()] = inx
742+ }
743+ if l.width > 0 && l.height > 0 {
744+ cmds = append(cmds, item.SetSize(l.width, l.height))
745+ }
746+ cmds = append(cmds, l.render())
747+ if l.direction == DirectionForward {
748+ if l.offset == 0 {
749+ cmd := l.GoToTop()
750+ if cmd != nil {
751+ cmds = append(cmds, cmd)
752+ }
753+ } else {
754+ newItem := l.renderedItems[item.ID()]
755+ newLines := newItem.height
756+ if len(l.items) > 1 {
757+ newLines += l.gap
758+ }
759+ l.offset = min(lipgloss.Height(l.rendered)-1, l.offset+newLines)
760+ }
761+ }
762+ return tea.Batch(cmds...)
656}
763}
657
764
658// SelectItemAbove implements List.
765// SelectItemAbove implements List.
@@ -707,7 +814,12 @@ func (l *list[T]) SelectedItem() *T {
707// SetItems implements List.
814// SetItems implements List.
708func (l *list[T]) SetItems(items []T) tea.Cmd {
815func (l *list[T]) SetItems(items []T) tea.Cmd {
709 l.items = items
816 l.items = items
710− return l.reset()
817+ var cmds []tea.Cmd
818+ for _, item := range l.items {
819+ cmds = append(cmds, item.Init())
820+ }
821+ cmds = append(cmds, l.reset())
822+ return tea.Batch(cmds...)
711}
823}
712
824
713// SetSelected implements List.
825// SetSelected implements List.
@@ -745,6 +857,42 @@ func (l *list[T]) SetSize(width int, height int) tea.Cmd {
745}
857}
746
858
747// UpdateItem implements List.
859// UpdateItem implements List.
748−func (l *list[T]) UpdateItem(string, T) tea.Cmd {
860+func (l *list[T]) UpdateItem(id string, item T) tea.Cmd {
749− panic("unimplemented")
861+ var cmds []tea.Cmd
862+ if inx, ok := l.indexMap[id]; ok {
863+ l.items[inx] = item
864+ oldItem := l.renderedItems[id]
865+ oldPosition := l.offset
866+ if l.direction == DirectionBackward {
867+ oldPosition = (lipgloss.Height(l.rendered) - 1) - l.offset
868+ }
869+
870+ delete(l.renderedItems, id)
871+ cmd := l.render()
872+
873+ // need to check for nil because of sequence not handling nil
874+ if cmd != nil {
875+ cmds = append(cmds, cmd)
876+ }
877+ if l.direction == DirectionBackward {
878+ // if we are the last item and there is no offset
879+ // make sure to go to the bottom
880+ if inx == len(l.items)-1 && l.offset == 0 {
881+ cmd = l.GoToBottom()
882+ if cmd != nil {
883+ cmds = append(cmds, cmd)
884+ }
885+ // if the item is at least partially below the viewport
886+ } else if oldPosition < oldItem.end {
887+ newItem := l.renderedItems[item.ID()]
888+ newLines := newItem.height - oldItem.height
889+ l.offset = util.Clamp(l.offset+newLines, 0, lipgloss.Height(l.rendered)-1)
890+ }
891+ } else if l.offset > oldItem.start {
892+ newItem := l.renderedItems[item.ID()]
893+ newLines := newItem.height - oldItem.height
894+ l.offset = util.Clamp(l.offset+newLines, 0, lipgloss.Height(l.rendered)-1)
895+ }
896+ }
897+ return tea.Sequence(cmds...)
750}
898}
internal/tui/exp/list/list_test.go+237
@@ -315,6 +315,243 @@ func TestListMovement(t *testing.T) {
315 assert.Equal(t, 0, l.offset)
315 assert.Equal(t, 0, l.offset)
316 golden.RequireEqual(t, []byte(l.View()))
316 golden.RequireEqual(t, []byte(l.View()))
317 })
317 })
318+
319+ t.Run("should not change offset when new items are appended and we are at the bottom in backwards list", func(t *testing.T) {
320+ t.Parallel()
321+ items := []Item{}
322+ for i := range 30 {
323+ content := strings.Repeat(fmt.Sprintf("Item %d\n", i), i+1)
324+ content = strings.TrimSuffix(content, "\n")
325+ item := NewSelectableItem(content)
326+ items = append(items, item)
327+ }
328+ l := New(items, WithDirectionBackward(), WithSize(10, 10)).(*list[Item])
329+ execCmd(l, l.Init())
330+ execCmd(l, l.AppendItem(NewSelectableItem("Testing")))
331+
332+ assert.Equal(t, 0, l.offset)
333+ golden.RequireEqual(t, []byte(l.View()))
334+ })
335+
336+ t.Run("should stay at the position it is when new items are added but we moved up in backwards list", func(t *testing.T) {
337+ t.Parallel()
338+ items := []Item{}
339+ for i := range 30 {
340+ item := NewSelectableItem(fmt.Sprintf("Item %d", i))
341+ items = append(items, item)
342+ }
343+ l := New(items, WithDirectionBackward(), WithSize(10, 10)).(*list[Item])
344+ execCmd(l, l.Init())
345+
346+ execCmd(l, l.MoveUp(2))
347+ viewBefore := l.View()
348+ execCmd(l, l.AppendItem(NewSelectableItem("Testing\nHello\n")))
349+ viewAfter := l.View()
350+ assert.Equal(t, viewBefore, viewAfter)
351+ assert.Equal(t, 5, l.offset)
352+ assert.Equal(t, 33, lipgloss.Height(l.rendered))
353+ golden.RequireEqual(t, []byte(l.View()))
354+ })
355+ t.Run("should stay at the position it is when the hight of an item below is increased in backwards list", func(t *testing.T) {
356+ t.Parallel()
357+ items := []Item{}
358+ for i := range 30 {
359+ item := NewSelectableItem(fmt.Sprintf("Item %d", i))
360+ items = append(items, item)
361+ }
362+ l := New(items, WithDirectionBackward(), WithSize(10, 10)).(*list[Item])
363+ execCmd(l, l.Init())
364+
365+ execCmd(l, l.MoveUp(2))
366+ viewBefore := l.View()
367+ item := items[29]
368+ execCmd(l, l.UpdateItem(item.ID(), NewSelectableItem("Item 29\nLine 2\nLine 3")))
369+ viewAfter := l.View()
370+ assert.Equal(t, viewBefore, viewAfter)
371+ assert.Equal(t, 4, l.offset)
372+ assert.Equal(t, 32, lipgloss.Height(l.rendered))
373+ golden.RequireEqual(t, []byte(l.View()))
374+ })
375+ t.Run("should stay at the position it is when the hight of an item below is decreases in backwards list", func(t *testing.T) {
376+ t.Parallel()
377+ items := []Item{}
378+ for i := range 30 {
379+ item := NewSelectableItem(fmt.Sprintf("Item %d", i))
380+ items = append(items, item)
381+ }
382+ items = append(items, NewSelectableItem("Item 30\nLine 2\nLine 3"))
383+ l := New(items, WithDirectionBackward(), WithSize(10, 10)).(*list[Item])
384+ execCmd(l, l.Init())
385+
386+ execCmd(l, l.MoveUp(2))
387+ viewBefore := l.View()
388+ item := items[30]
389+ execCmd(l, l.UpdateItem(item.ID(), NewSelectableItem("Item 30")))
390+ viewAfter := l.View()
391+ assert.Equal(t, viewBefore, viewAfter)
392+ assert.Equal(t, 0, l.offset)
393+ assert.Equal(t, 31, lipgloss.Height(l.rendered))
394+ golden.RequireEqual(t, []byte(l.View()))
395+ })
396+ t.Run("should stay at the position it is when the hight of an item above is increased in backwards list", func(t *testing.T) {
397+ t.Parallel()
398+ items := []Item{}
399+ for i := range 30 {
400+ item := NewSelectableItem(fmt.Sprintf("Item %d", i))
401+ items = append(items, item)
402+ }
403+ l := New(items, WithDirectionBackward(), WithSize(10, 10)).(*list[Item])
404+ execCmd(l, l.Init())
405+
406+ execCmd(l, l.MoveUp(2))
407+ viewBefore := l.View()
408+ item := items[1]
409+ execCmd(l, l.UpdateItem(item.ID(), NewSelectableItem("Item 1\nLine 2\nLine 3")))
410+ viewAfter := l.View()
411+ assert.Equal(t, viewBefore, viewAfter)
412+ assert.Equal(t, 2, l.offset)
413+ assert.Equal(t, 32, lipgloss.Height(l.rendered))
414+ golden.RequireEqual(t, []byte(l.View()))
415+ })
416+ t.Run("should stay at the position it is if an item is prepended and we are in backwards list", func(t *testing.T) {
417+ t.Parallel()
418+ items := []Item{}
419+ for i := range 30 {
420+ item := NewSelectableItem(fmt.Sprintf("Item %d", i))
421+ items = append(items, item)
422+ }
423+ l := New(items, WithDirectionBackward(), WithSize(10, 10)).(*list[Item])
424+ execCmd(l, l.Init())
425+
426+ execCmd(l, l.MoveUp(2))
427+ viewBefore := l.View()
428+ execCmd(l, l.PrependItem(NewSelectableItem("New")))
429+ viewAfter := l.View()
430+ assert.Equal(t, viewBefore, viewAfter)
431+ assert.Equal(t, 2, l.offset)
432+ assert.Equal(t, 31, lipgloss.Height(l.rendered))
433+ golden.RequireEqual(t, []byte(l.View()))
434+ })
435+
436+ t.Run("should not change offset when new items are prepended and we are at the top in forward list", func(t *testing.T) {
437+ t.Parallel()
438+ items := []Item{}
439+ for i := range 30 {
440+ content := strings.Repeat(fmt.Sprintf("Item %d\n", i), i+1)
441+ content = strings.TrimSuffix(content, "\n")
442+ item := NewSelectableItem(content)
443+ items = append(items, item)
444+ }
445+ l := New(items, WithDirectionForward(), WithSize(10, 10)).(*list[Item])
446+ execCmd(l, l.Init())
447+ execCmd(l, l.PrependItem(NewSelectableItem("Testing")))
448+
449+ assert.Equal(t, 0, l.offset)
450+ golden.RequireEqual(t, []byte(l.View()))
451+ })
452+
453+ t.Run("should stay at the position it is when new items are added but we moved down in forward list", func(t *testing.T) {
454+ t.Parallel()
455+ items := []Item{}
456+ for i := range 30 {
457+ item := NewSelectableItem(fmt.Sprintf("Item %d", i))
458+ items = append(items, item)
459+ }
460+ l := New(items, WithDirectionForward(), WithSize(10, 10)).(*list[Item])
461+ execCmd(l, l.Init())
462+
463+ execCmd(l, l.MoveDown(2))
464+ viewBefore := l.View()
465+ execCmd(l, l.PrependItem(NewSelectableItem("Testing\nHello\n")))
466+ viewAfter := l.View()
467+ assert.Equal(t, viewBefore, viewAfter)
468+ assert.Equal(t, 5, l.offset)
469+ assert.Equal(t, 33, lipgloss.Height(l.rendered))
470+ golden.RequireEqual(t, []byte(l.View()))
471+ })
472+
473+ t.Run("should stay at the position it is when the hight of an item above is increased in forward list", func(t *testing.T) {
474+ t.Parallel()
475+ items := []Item{}
476+ for i := range 30 {
477+ item := NewSelectableItem(fmt.Sprintf("Item %d", i))
478+ items = append(items, item)
479+ }
480+ l := New(items, WithDirectionForward(), WithSize(10, 10)).(*list[Item])
481+ execCmd(l, l.Init())
482+
483+ execCmd(l, l.MoveDown(2))
484+ viewBefore := l.View()
485+ item := items[0]
486+ execCmd(l, l.UpdateItem(item.ID(), NewSelectableItem("Item 29\nLine 2\nLine 3")))
487+ viewAfter := l.View()
488+ assert.Equal(t, viewBefore, viewAfter)
489+ assert.Equal(t, 4, l.offset)
490+ assert.Equal(t, 32, lipgloss.Height(l.rendered))
491+ golden.RequireEqual(t, []byte(l.View()))
492+ })
493+
494+ t.Run("should stay at the position it is when the hight of an item above is decreases in forward list", func(t *testing.T) {
495+ t.Parallel()
496+ items := []Item{}
497+ items = append(items, NewSelectableItem("At top\nLine 2\nLine 3"))
498+ for i := range 30 {
499+ item := NewSelectableItem(fmt.Sprintf("Item %d", i))
500+ items = append(items, item)
501+ }
502+ l := New(items, WithDirectionForward(), WithSize(10, 10)).(*list[Item])
503+ execCmd(l, l.Init())
504+
505+ execCmd(l, l.MoveDown(3))
506+ viewBefore := l.View()
507+ item := items[0]
508+ execCmd(l, l.UpdateItem(item.ID(), NewSelectableItem("At top")))
509+ viewAfter := l.View()
510+ assert.Equal(t, viewBefore, viewAfter)
511+ assert.Equal(t, 1, l.offset)
512+ assert.Equal(t, 31, lipgloss.Height(l.rendered))
513+ golden.RequireEqual(t, []byte(l.View()))
514+ })
515+
516+ t.Run("should stay at the position it is when the hight of an item below is increased in forward list", func(t *testing.T) {
517+ t.Parallel()
518+ items := []Item{}
519+ for i := range 30 {
520+ item := NewSelectableItem(fmt.Sprintf("Item %d", i))
521+ items = append(items, item)
522+ }
523+ l := New(items, WithDirectionForward(), WithSize(10, 10)).(*list[Item])
524+ execCmd(l, l.Init())
525+
526+ execCmd(l, l.MoveDown(2))
527+ viewBefore := l.View()
528+ item := items[29]
529+ execCmd(l, l.UpdateItem(item.ID(), NewSelectableItem("Item 29\nLine 2\nLine 3")))
530+ viewAfter := l.View()
531+ assert.Equal(t, viewBefore, viewAfter)
532+ assert.Equal(t, 2, l.offset)
533+ assert.Equal(t, 32, lipgloss.Height(l.rendered))
534+ golden.RequireEqual(t, []byte(l.View()))
535+ })
536+ t.Run("should stay at the position it is if an item is appended and we are in forward list", func(t *testing.T) {
537+ t.Parallel()
538+ items := []Item{}
539+ for i := range 30 {
540+ item := NewSelectableItem(fmt.Sprintf("Item %d", i))
541+ items = append(items, item)
542+ }
543+ l := New(items, WithDirectionForward(), WithSize(10, 10)).(*list[Item])
544+ execCmd(l, l.Init())
545+
546+ execCmd(l, l.MoveDown(2))
547+ viewBefore := l.View()
548+ execCmd(l, l.AppendItem(NewSelectableItem("New")))
549+ viewAfter := l.View()
550+ assert.Equal(t, viewBefore, viewAfter)
551+ assert.Equal(t, 2, l.offset)
552+ assert.Equal(t, 31, lipgloss.Height(l.rendered))
553+ golden.RequireEqual(t, []byte(l.View()))
554+ })
318}
555}
319
556
320type SelectableItem interface {
557type SelectableItem interface {
internal/tui/exp/list/testdata/TestListMovement/should_not_change_offset_when_new_items_are_appended_and_we_are_at_the_bottom_in_backwards_list.goldennew file+10
@@ -0,0 +1,10 @@
1+Item 29
2+Item 29
3+Item 29
4+Item 29
5+Item 29
6+Item 29
7+Item 29
8+Item 29
9+Item 29
10+│Testing
internal/tui/exp/list/testdata/TestListMovement/should_not_change_offset_when_new_items_are_appended_and_we_are_at_the_botton.goldennew file+10
@@ -0,0 +1,10 @@
1+Item 29
2+Item 29
3+Item 29
4+Item 29
5+Item 29
6+Item 29
7+Item 29
8+Item 29
9+Item 29
10+│Testing
internal/tui/exp/list/testdata/TestListMovement/should_not_change_offset_when_new_items_are_prepended_and_we_are_at_the_top_in_forward_list.goldennew file+10
@@ -0,0 +1,10 @@
1+│Testing
2+Item 0
3+Item 1
4+Item 1
5+Item 2
6+Item 2
7+Item 2
8+Item 3
9+Item 3
10+Item 3
internal/tui/exp/list/testdata/TestListMovement/should_stay_at_the_position_it_is_if_an_item_is_appended_and_we_are_in_forward_list.goldennew file+10
@@ -0,0 +1,10 @@
1+│Item 2
2+Item 3
3+Item 4
4+Item 5
5+Item 6
6+Item 7
7+Item 8
8+Item 9
9+Item 10
10+Item 11
internal/tui/exp/list/testdata/TestListMovement/should_stay_at_the_position_it_is_if_an_item_is_prepended_and_we_are_in_backwards_list.goldennew file+10
@@ -0,0 +1,10 @@
1+Item 18
2+Item 19
3+Item 20
4+Item 21
5+Item 22
6+Item 23
7+Item 24
8+Item 25
9+Item 26
10+│Item 27
internal/tui/exp/list/testdata/TestListMovement/should_stay_at_the_position_it_is_when_new_items_are_added_but_we_moved_down_in_forward_list.goldennew file+10
@@ -0,0 +1,10 @@
1+│Item 2
2+Item 3
3+Item 4
4+Item 5
5+Item 6
6+Item 7
7+Item 8
8+Item 9
9+Item 10
10+Item 11
internal/tui/exp/list/testdata/TestListMovement/should_stay_at_the_position_it_is_when_new_items_are_added_but_we_moved_up.goldennew file+10
@@ -0,0 +1,10 @@
1+Item 18
2+Item 19
3+Item 20
4+Item 21
5+Item 22
6+Item 23
7+Item 24
8+Item 25
9+Item 26
10+│Item 27
internal/tui/exp/list/testdata/TestListMovement/should_stay_at_the_position_it_is_when_new_items_are_added_but_we_moved_up_in_backwards_list.goldennew file+10
@@ -0,0 +1,10 @@
1+Item 18
2+Item 19
3+Item 20
4+Item 21
5+Item 22
6+Item 23
7+Item 24
8+Item 25
9+Item 26
10+│Item 27
internal/tui/exp/list/testdata/TestListMovement/should_stay_at_the_position_it_is_when_the_hight_of_an_item_above_is_decreases_in_forward_list.goldennew file+10
@@ -0,0 +1,10 @@
1+│Item 0
2+Item 1
3+Item 2
4+Item 3
5+Item 4
6+Item 5
7+Item 6
8+Item 7
9+Item 8
10+Item 9
internal/tui/exp/list/testdata/TestListMovement/should_stay_at_the_position_it_is_when_the_hight_of_an_item_above_is_increased.goldennew file+10
@@ -0,0 +1,10 @@
1+Item 18
2+Item 19
3+Item 20
4+Item 21
5+Item 22
6+Item 23
7+Item 24
8+Item 25
9+Item 26
10+│Item 27
internal/tui/exp/list/testdata/TestListMovement/should_stay_at_the_position_it_is_when_the_hight_of_an_item_above_is_increased_in_backwards_list.goldennew file+10
@@ -0,0 +1,10 @@
1+Item 18
2+Item 19
3+Item 20
4+Item 21
5+Item 22
6+Item 23
7+Item 24
8+Item 25
9+Item 26
10+│Item 27
internal/tui/exp/list/testdata/TestListMovement/should_stay_at_the_position_it_is_when_the_hight_of_an_item_above_is_increased_in_forward_list.goldennew file+10
@@ -0,0 +1,10 @@
1+│Item 2
2+Item 3
3+Item 4
4+Item 5
5+Item 6
6+Item 7
7+Item 8
8+Item 9
9+Item 10
10+Item 11
internal/tui/exp/list/testdata/TestListMovement/should_stay_at_the_position_it_is_when_the_hight_of_an_item_below_is_decreases.goldennew file+10
@@ -0,0 +1,10 @@
1+Item 21
2+Item 22
3+Item 23
4+Item 24
5+Item 25
6+Item 26
7+Item 27
8+Item 28
9+│Item 29
10+Item 30
internal/tui/exp/list/testdata/TestListMovement/should_stay_at_the_position_it_is_when_the_hight_of_an_item_below_is_decreases_in_backwards_list.goldennew file+10
@@ -0,0 +1,10 @@
1+Item 21
2+Item 22
3+Item 23
4+Item 24
5+Item 25
6+Item 26
7+Item 27
8+Item 28
9+│Item 29
10+Item 30
internal/tui/exp/list/testdata/TestListMovement/should_stay_at_the_position_it_is_when_the_hight_of_an_item_below_is_increased#01.goldennew file+10
@@ -0,0 +1,10 @@
1+Item 18
2+Item 19
3+Item 20
4+Item 21
5+Item 22
6+Item 23
7+Item 24
8+Item 25
9+Item 26
10+│Item 27
internal/tui/exp/list/testdata/TestListMovement/should_stay_at_the_position_it_is_when_the_hight_of_an_item_below_is_increased.goldennew file+10
@@ -0,0 +1,10 @@
1+Item 18
2+Item 19
3+Item 20
4+Item 21
5+Item 22
6+Item 23
7+Item 24
8+Item 25
9+Item 26
10+│Item 27
internal/tui/exp/list/testdata/TestListMovement/should_stay_at_the_position_it_is_when_the_hight_of_an_item_below_is_increased_in_backwards_list.goldennew file+10
@@ -0,0 +1,10 @@
1+Item 18
2+Item 19
3+Item 20
4+Item 21
5+Item 22
6+Item 23
7+Item 24
8+Item 25
9+Item 26
10+│Item 27
internal/tui/exp/list/testdata/TestListMovement/should_stay_at_the_position_it_is_when_the_hight_of_an_item_below_is_increased_in_forward_list.goldennew file+10
@@ -0,0 +1,10 @@
1+│Item 2
2+Item 3
3+Item 4
4+Item 5
5+Item 6
6+Item 7
7+Item 8
8+Item 9
9+Item 10
10+Item 11
internal/tui/page/chat/chat.go+1
@@ -604,6 +604,7 @@ func (p *chatPage) sendMessage(text string, attachments []message.Attachment) te
604 if err != nil {
604 if err != nil {
605 return util.ReportError(err)
605 return util.ReportError(err)
606 }
606 }
607+ cmds = append(cmds, p.chat.GoToBottom())
607 return tea.Batch(cmds...)
608 return tea.Batch(cmds...)
608}
609}
609
610