diff --git a/internal/tui/components/chat/chat.go b/internal/tui/components/chat/chat.go index eaff4c7e3b697abd18aa9b2831ed2814e4c7c2cf..3be9a9fd913f33cdce167e283c88275ffed14ad9 100644 --- a/internal/tui/components/chat/chat.go +++ b/internal/tui/components/chat/chat.go @@ -46,8 +46,7 @@ type messageListCmp struct { width, height int session session.Session listCmp list.ListModel - focused bool // Focus state for styling - previousSelected int // Last selected item index for restoring focus + previousSelected int // Last selected item index for restoring focus lastUserMessageTime int64 } @@ -56,20 +55,21 @@ type messageListCmp struct { // and reverse ordering (newest messages at bottom). func NewMessagesListCmp(app *app.App) MessageListCmp { defaultKeymaps := list.DefaultKeyMap() + listCmp := list.New( + list.WithGapSize(1), + list.WithReverse(true), + list.WithKeyMap(defaultKeymaps), + ) return &messageListCmp{ - app: app, - listCmp: list.New( - list.WithGapSize(1), - list.WithReverse(true), - list.WithKeyMap(defaultKeymaps), - ), + app: app, + listCmp: listCmp, previousSelected: list.NoSelection, } } // Init initializes the component (no initialization needed). func (m *messageListCmp) Init() tea.Cmd { - return nil + return tea.Sequence(m.listCmp.Init(), m.listCmp.Blur()) } // Update handles incoming messages and updates the component state. @@ -483,24 +483,15 @@ func (m *messageListCmp) SetSize(width int, height int) tea.Cmd { // Blur implements MessageListCmp. func (m *messageListCmp) Blur() tea.Cmd { - m.focused = false - m.previousSelected = m.listCmp.SelectedIndex() - m.listCmp.ClearSelection() - return nil + return m.listCmp.Blur() } // Focus implements MessageListCmp. func (m *messageListCmp) Focus() tea.Cmd { - m.focused = true - if m.previousSelected != list.NoSelection { - m.listCmp.SetSelected(m.previousSelected) - } else { - m.listCmp.SetSelected(len(m.listCmp.Items()) - 1) - } - return nil + return m.listCmp.Focus() } // IsFocused implements MessageListCmp. func (m *messageListCmp) IsFocused() bool { - return m.focused + return m.listCmp.IsFocused() } diff --git a/internal/tui/components/core/list/list.go b/internal/tui/components/core/list/list.go index cfbcc89033d49dd7cdb8af32351dd1cedcbe27ec..8f22ccc7c8f73b16ff47f85882e1ee4bc3e2c8bf 100644 --- a/internal/tui/components/core/list/list.go +++ b/internal/tui/components/core/list/list.go @@ -31,6 +31,7 @@ const ( type ListModel interface { util.Model layout.Sizeable + layout.Focusable SetItems([]util.Model) tea.Cmd // Replace all items in the list AppendItem(util.Model) tea.Cmd // Add an item to the end of the list PrependItem(util.Model) tea.Cmd // Add an item to the beginning of the list @@ -40,7 +41,6 @@ type ListModel interface { Items() []util.Model // Get all items in the list SelectedIndex() int // Get the index of the currently selected item SetSelected(int) tea.Cmd // Set the selected item by index and scroll to it - ClearSelection() tea.Cmd // Clear the current selection Filter(string) tea.Cmd // Filter items based on a search term } @@ -143,6 +143,8 @@ type model struct { inputStyle lipgloss.Style // Style for the input field hideFilterInput bool // Whether to hide the filter input field currentSearch string // Current search term for filtering + + isFocused bool // Whether the list is currently focused } // listOptions is a function type for configuring list options. @@ -238,6 +240,7 @@ func New(opts ...listOptions) ListModel { selectionState: selectionState{selectedIndex: NoSelection}, filterPlaceholder: "Type to filter...", inputStyle: t.S().Base.Padding(0, 1, 1, 1), + isFocused: true, } for _, opt := range opts { opt(m) @@ -746,8 +749,10 @@ func (m *model) ensureVisibleReverse(cachedItem renderedItem) { func (m *model) goToBottom() tea.Cmd { cmds := []tea.Cmd{m.blurSelected()} m.viewState.reverse = true - m.selectionState.selectedIndex = m.findLastSelectableItem() - cmds = append(cmds, m.focusSelected()) + if m.isFocused { + m.selectionState.selectedIndex = m.findLastSelectableItem() + cmds = append(cmds, m.focusSelected()) + } m.ResetView() return tea.Batch(cmds...) } @@ -774,6 +779,9 @@ func (m *model) ResetView() { // focusSelected gives focus to the currently selected item if it supports focus. // Triggers a re-render of the item to show its focused state. func (m *model) focusSelected() tea.Cmd { + if !m.isFocused { + return nil // No focus change if the list is not focused + } if !m.selectionState.isValidIndex(len(m.filteredItems)) { return nil } @@ -953,9 +961,7 @@ func (m *model) UpdateItem(inx int, item util.Model) { } m.filteredItems[inx] = item if m.selectionState.selectedIndex == inx { - if i, ok := m.filteredItems[m.selectionState.selectedIndex].(layout.Focusable); ok { - i.Focus() - } + m.focusSelected() } m.setItemSize(inx) m.rerenderItem(inx) @@ -1334,14 +1340,21 @@ func (m *model) SetSelected(index int) tea.Cmd { return tea.Batch(cmds...) } -// ClearSelection clears the current selection and focus. -func (m *model) ClearSelection() tea.Cmd { - cmds := []tea.Cmd{} - if m.selectionState.selectedIndex >= 0 && m.selectionState.selectedIndex < len(m.filteredItems) { - if i, ok := m.filteredItems[m.selectionState.selectedIndex].(layout.Focusable); ok { - cmds = append(cmds, i.Blur()) - } - } - m.selectionState.selectedIndex = NoSelection - return tea.Batch(cmds...) +// Blur implements ListModel. +func (m *model) Blur() tea.Cmd { + m.isFocused = false + cmd := m.blurSelected() + return cmd +} + +// Focus implements ListModel. +func (m *model) Focus() tea.Cmd { + m.isFocused = true + cmd := m.focusSelected() + return cmd +} + +// IsFocused implements ListModel. +func (m *model) IsFocused() bool { + return m.isFocused }