fix(chat): fix focus selection

Kujtim Hoxha created

Change summary

internal/tui/components/chat/chat.go      | 33 ++++++-----------
internal/tui/components/core/list/list.go | 45 ++++++++++++++++--------
2 files changed, 41 insertions(+), 37 deletions(-)

Detailed changes

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()
 }

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
 }