Detailed changes
@@ -818,6 +818,49 @@ func (l *List) TotalHeight() int {
return l.totalHeight
}
+// SelectFirstInView selects the first item that is fully visible in the viewport.
+func (l *List) SelectFirstInView() {
+ l.ensureBuilt()
+
+ viewportStart := l.offset
+ viewportEnd := l.offset + l.height
+
+ for i, item := range l.items {
+ pos, ok := l.itemPositions[item.ID()]
+ if !ok {
+ continue
+ }
+
+ // Check if item is fully within viewport bounds
+ if pos.startLine >= viewportStart && (pos.startLine+pos.height) <= viewportEnd {
+ l.SetSelectedIndex(i)
+ return
+ }
+ }
+}
+
+// SelectLastInView selects the last item that is fully visible in the viewport.
+func (l *List) SelectLastInView() {
+ l.ensureBuilt()
+
+ viewportStart := l.offset
+ 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
+ }
+
+ // Check if item is fully within viewport bounds
+ if pos.startLine >= viewportStart && (pos.startLine+pos.height) <= viewportEnd {
+ l.SetSelectedIndex(i)
+ return
+ }
+ }
+}
+
// SelectedItemInView returns true if the selected item is currently visible in the viewport.
func (l *List) SelectedItemInView() bool {
if l.selectedIdx < 0 || l.selectedIdx >= len(l.items) {
@@ -77,9 +77,6 @@ func NewChatNoContentItem(t *styles.Styles, id string) *ChatNoContentItem {
// ChatMessageItem represents a chat message item in the chat UI.
type ChatMessageItem struct {
- list.BaseFocusable
- list.BaseHighlightable
-
item list.Item
msg message.Message
}
@@ -169,6 +166,43 @@ 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 {
+ blurable.Blur()
+ }
+}
+
+// Focus implements list.Focusable.
+func (c *ChatMessageItem) Focus() {
+ if focusable, ok := c.item.(list.Focusable); ok {
+ focusable.Focus()
+ }
+}
+
+// IsFocused implements list.Focusable.
+func (c *ChatMessageItem) IsFocused() bool {
+ if focusable, ok := c.item.(list.Focusable); ok {
+ return focusable.IsFocused()
+ }
+ return false
+}
+
+// GetHighlight implements list.Highlightable.
+func (c *ChatMessageItem) GetHighlight() (startLine int, startCol int, endLine int, endCol int) {
+ if highlightable, ok := c.item.(list.Highlightable); ok {
+ return highlightable.GetHighlight()
+ }
+ return 0, 0, 0, 0
+}
+
+// SetHighlight implements list.Highlightable.
+func (c *ChatMessageItem) SetHighlight(startLine int, startCol int, endLine int, endCol int) {
+ if highlightable, ok := c.item.(list.Highlightable); ok {
+ highlightable.SetHighlight(startLine, startCol, endLine, endCol)
+ }
+}
+
// Chat represents the chat UI model that handles chat interactions and
// messages.
type Chat struct {
@@ -279,6 +313,26 @@ func (m *Chat) SelectNext() {
m.list.SelectNext()
}
+// SelectFirst selects the first message in the chat list.
+func (m *Chat) SelectFirst() {
+ m.list.SelectFirst()
+}
+
+// SelectLast selects the last message in the chat list.
+func (m *Chat) SelectLast() {
+ m.list.SelectLast()
+}
+
+// SelectFirstInView selects the first message currently in view.
+func (m *Chat) SelectFirstInView() {
+ m.list.SelectFirstInView()
+}
+
+// SelectLastInView selects the last message currently in view.
+func (m *Chat) SelectLastInView() {
+ m.list.SelectLastInView()
+}
+
// HandleMouseDown handles mouse down events for the chat component.
func (m *Chat) HandleMouseDown(x, y int) {
m.list.HandleMouseDown(x, y)
@@ -23,6 +23,16 @@ type KeyMap struct {
Cancel key.Binding
Tab key.Binding
Details key.Binding
+ Down key.Binding
+ Up key.Binding
+ DownOneItem key.Binding
+ UpOneItem key.Binding
+ PageDown key.Binding
+ PageUp key.Binding
+ HalfPageDown key.Binding
+ HalfPageUp key.Binding
+ Home key.Binding
+ End key.Binding
}
Initialize struct {
@@ -135,6 +145,47 @@ func DefaultKeyMap() KeyMap {
key.WithHelp("ctrl+d", "toggle details"),
)
+ km.Chat.Down = key.NewBinding(
+ key.WithKeys("down", "ctrl+j", "ctrl+n", "j"),
+ key.WithHelp("↓", "down"),
+ )
+ km.Chat.Up = key.NewBinding(
+ key.WithKeys("up", "ctrl+k", "ctrl+p", "k"),
+ key.WithHelp("↑", "up"),
+ )
+ km.Chat.UpOneItem = key.NewBinding(
+ key.WithKeys("shift+up", "K"),
+ key.WithHelp("shift+↑", "up one item"),
+ )
+ km.Chat.DownOneItem = key.NewBinding(
+ key.WithKeys("shift+down", "J"),
+ key.WithHelp("shift+↓", "down one item"),
+ )
+ km.Chat.HalfPageDown = key.NewBinding(
+ key.WithKeys("d"),
+ key.WithHelp("d", "half page down"),
+ )
+ km.Chat.PageDown = key.NewBinding(
+ key.WithKeys("pgdown", " ", "f"),
+ key.WithHelp("f/pgdn", "page down"),
+ )
+ km.Chat.PageUp = key.NewBinding(
+ key.WithKeys("pgup", "b"),
+ key.WithHelp("b/pgup", "page up"),
+ )
+ km.Chat.HalfPageUp = key.NewBinding(
+ key.WithKeys("u"),
+ key.WithHelp("u", "half page up"),
+ )
+ km.Chat.Home = key.NewBinding(
+ key.WithKeys("g", "home"),
+ key.WithHelp("g", "home"),
+ )
+ km.Chat.End = key.NewBinding(
+ key.WithKeys("G", "end"),
+ key.WithHelp("G", "end"),
+ )
+
km.Initialize.Yes = key.NewBinding(
key.WithKeys("y", "Y"),
key.WithHelp("y", "yes"),
@@ -7,6 +7,7 @@ import (
"os"
"slices"
"strings"
+ "time"
"charm.land/bubbles/v2/help"
"charm.land/bubbles/v2/key"
@@ -177,7 +178,10 @@ func (m *UI) Init() tea.Cmd {
}
allSessions, _ := m.com.App.Sessions.List(context.Background())
if len(allSessions) > 0 {
- cmds = append(cmds, m.loadSession(allSessions[0].ID))
+ cmds = append(cmds, func() tea.Msg {
+ time.Sleep(2 * time.Second)
+ return m.loadSession(allSessions[0].ID)()
+ })
}
return tea.Batch(cmds...)
}
@@ -300,10 +304,30 @@ func (m *UI) handleKeyPressMsg(msg tea.KeyPressMsg) (cmds []tea.Cmd) {
return m.updateDialogs(msg)
}
- switch {
- case key.Matches(msg, m.keyMap.Tab):
- switch m.state {
- case uiChat:
+ handleGlobalKeys := func(msg tea.KeyPressMsg) {
+ switch {
+ case key.Matches(msg, m.keyMap.Tab):
+ case key.Matches(msg, m.keyMap.Help):
+ m.help.ShowAll = !m.help.ShowAll
+ m.updateLayoutAndSize()
+ case key.Matches(msg, m.keyMap.Quit):
+ if !m.dialog.ContainsDialog(dialog.QuitDialogID) {
+ m.dialog.AddDialog(dialog.NewQuit(m.com))
+ return
+ }
+ case key.Matches(msg, m.keyMap.Commands):
+ // TODO: Implement me
+ case key.Matches(msg, m.keyMap.Models):
+ // TODO: Implement me
+ case key.Matches(msg, m.keyMap.Sessions):
+ // TODO: Implement me
+ }
+ }
+
+ switch m.state {
+ case uiChat:
+ switch {
+ case key.Matches(msg, m.keyMap.Tab):
if m.focus == uiFocusMain {
m.focus = uiFocusEditor
cmds = append(cmds, m.textarea.Focus())
@@ -314,26 +338,47 @@ func (m *UI) handleKeyPressMsg(msg tea.KeyPressMsg) (cmds []tea.Cmd) {
m.chat.Focus()
m.chat.SetSelectedIndex(m.chat.Len() - 1)
}
+ case key.Matches(msg, m.keyMap.Chat.Up):
+ m.chat.ScrollBy(-1)
+ if !m.chat.SelectedItemInView() {
+ m.chat.SelectPrev()
+ m.chat.ScrollToSelected()
+ }
+ case key.Matches(msg, m.keyMap.Chat.Down):
+ m.chat.ScrollBy(1)
+ if !m.chat.SelectedItemInView() {
+ m.chat.SelectNext()
+ m.chat.ScrollToSelected()
+ }
+ case key.Matches(msg, m.keyMap.Chat.UpOneItem):
+ m.chat.SelectPrev()
+ m.chat.ScrollToSelected()
+ case key.Matches(msg, m.keyMap.Chat.DownOneItem):
+ m.chat.SelectNext()
+ m.chat.ScrollToSelected()
+ case key.Matches(msg, m.keyMap.Chat.HalfPageUp):
+ m.chat.ScrollBy(-m.chat.Height() / 2)
+ m.chat.SelectFirstInView()
+ case key.Matches(msg, m.keyMap.Chat.HalfPageDown):
+ m.chat.ScrollBy(m.chat.Height() / 2)
+ m.chat.SelectLastInView()
+ case key.Matches(msg, m.keyMap.Chat.PageUp):
+ m.chat.ScrollBy(-m.chat.Height())
+ m.chat.SelectFirstInView()
+ case key.Matches(msg, m.keyMap.Chat.PageDown):
+ m.chat.ScrollBy(m.chat.Height())
+ m.chat.SelectLastInView()
+ case key.Matches(msg, m.keyMap.Chat.Home):
+ m.chat.ScrollToTop()
+ m.chat.SelectFirst()
+ case key.Matches(msg, m.keyMap.Chat.End):
+ m.chat.ScrollToBottom()
+ m.chat.SelectLast()
+ default:
+ handleGlobalKeys(msg)
}
- case key.Matches(msg, m.keyMap.Help):
- m.help.ShowAll = !m.help.ShowAll
- m.updateLayoutAndSize()
- return cmds
- case key.Matches(msg, m.keyMap.Quit):
- if !m.dialog.ContainsDialog(dialog.QuitDialogID) {
- m.dialog.AddDialog(dialog.NewQuit(m.com))
- return
- }
- return cmds
- case key.Matches(msg, m.keyMap.Commands):
- // TODO: Implement me
- return cmds
- case key.Matches(msg, m.keyMap.Models):
- // TODO: Implement me
- return cmds
- case key.Matches(msg, m.keyMap.Sessions):
- // TODO: Implement me
- return cmds
+ default:
+ handleGlobalKeys(msg)
}
cmds = append(cmds, m.updateFocused(msg)...)