chore: change the todos focus to a toggle

Kujtim Hoxha created

Change summary

internal/tui/page/chat/chat.go | 234 +++++++++++++----------------------
internal/tui/page/chat/keys.go |  35 +---
2 files changed, 99 insertions(+), 170 deletions(-)

Detailed changes

internal/tui/page/chat/chat.go 🔗

@@ -55,7 +55,6 @@ const (
 	PanelTypeChat   PanelType = "chat"
 	PanelTypeEditor PanelType = "editor"
 	PanelTypeSplash PanelType = "splash"
-	PanelTypeStatus PanelType = "status"
 )
 
 // PillSection represents which pill section is focused when in pills panel.
@@ -109,9 +108,8 @@ type chatPage struct {
 	focusedPane  PanelType
 
 	// Session
-	session     session.Session
-	keyMap      KeyMap
-	pillsKeyMap PillsKeyMap
+	session session.Session
+	keyMap  KeyMap
 
 	// Components
 	header  header.Header
@@ -129,8 +127,8 @@ type chatPage struct {
 	promptQueue      int
 
 	// Pills state
-	focusedPillSection  PillSection
-	previousFocusedPane PanelType
+	pillsExpanded      bool
+	focusedPillSection PillSection
 
 	// Todo spinner
 	todoSpinner spinner.Model
@@ -141,7 +139,6 @@ func New(app *app.App) ChatPage {
 	return &chatPage{
 		app:         app,
 		keyMap:      DefaultKeyMap(),
-		pillsKeyMap: DefaultPillsKeyMap(),
 		header:      header.New(app.LSPClients),
 		sidebar:     sidebar.New(app.History, app.LSPClients, false),
 		chat:        chat.New(app),
@@ -464,6 +461,18 @@ func (p *chatPage) Update(msg tea.Msg) (util.Model, tea.Cmd) {
 		case key.Matches(msg, p.keyMap.Details):
 			p.toggleDetails()
 			return p, nil
+		case key.Matches(msg, p.keyMap.TogglePills):
+			if p.session.ID != "" {
+				return p, p.togglePillsExpanded()
+			}
+		case key.Matches(msg, p.keyMap.PillLeft):
+			if p.session.ID != "" && p.pillsExpanded {
+				return p, p.switchPillSection(-1)
+			}
+		case key.Matches(msg, p.keyMap.PillRight):
+			if p.session.ID != "" && p.pillsExpanded {
+				return p, p.switchPillSection(1)
+			}
 		}
 
 		switch p.focusedPane {
@@ -479,8 +488,6 @@ func (p *chatPage) Update(msg tea.Msg) (util.Model, tea.Cmd) {
 			u, cmd := p.splash.Update(msg)
 			p.splash = u.(splash.Splash)
 			cmds = append(cmds, cmd)
-		case PanelTypeStatus:
-			cmds = append(cmds, p.handlePillsKeyPress(msg))
 		}
 	case tea.PasteMsg:
 		switch p.focusedPane {
@@ -542,9 +549,8 @@ func (p *chatPage) View() string {
 
 		hasIncompleteTodos := hasIncompleteTodos(p.session.Todos)
 		hasQueue := p.promptQueue > 0
-		pillsFocused := p.focusedPane == PanelTypeStatus
-		todosFocused := pillsFocused && p.focusedPillSection == PillSectionTodos
-		queueFocused := pillsFocused && p.focusedPillSection == PillSectionQueue
+		todosFocused := p.pillsExpanded && p.focusedPillSection == PillSectionTodos
+		queueFocused := p.pillsExpanded && p.focusedPillSection == PillSectionQueue
 
 		// Use spinner when agent is busy, otherwise show static icon
 		agentBusy := p.app.AgentCoordinator != nil && p.app.AgentCoordinator.IsBusy()
@@ -555,14 +561,14 @@ func (p *chatPage) View() string {
 
 		var pills []string
 		if hasIncompleteTodos {
-			pills = append(pills, todoPill(p.session.Todos, inProgressIcon, todosFocused, pillsFocused, t))
+			pills = append(pills, todoPill(p.session.Todos, inProgressIcon, todosFocused, p.pillsExpanded, t))
 		}
 		if hasQueue {
-			pills = append(pills, queuePill(p.promptQueue, queueFocused, pillsFocused, t))
+			pills = append(pills, queuePill(p.promptQueue, queueFocused, p.pillsExpanded, t))
 		}
 
 		var expandedList string
-		if pillsFocused {
+		if p.pillsExpanded {
 			if todosFocused && hasIncompleteTodos {
 				expandedList = todoList(p.session.Todos, inProgressIcon, t, p.width-SideBarWidth)
 			} else if queueFocused && hasQueue {
@@ -575,11 +581,11 @@ func (p *chatPage) View() string {
 		if len(pills) > 0 {
 			pillsRow := lipgloss.JoinHorizontal(lipgloss.Top, pills...)
 
-			// Add section line only when focused
-			if pillsFocused {
+			// Add section line only when expanded
+			if p.pillsExpanded {
 				// Calculate available width for section line after pills
 				totalWidth := p.width - SideBarWidth - 2 // -2 for left padding
-				totalWidth -= 2                          // Additional padding when focused
+				totalWidth -= 2                          // Additional padding when expanded
 				pillsRowWidth := lipgloss.Width(pillsRow)
 				availableWidth := totalWidth - pillsRowWidth
 
@@ -602,7 +608,7 @@ func (p *chatPage) View() string {
 			}
 
 			style := t.S().Base.MarginTop(1)
-			if pillsFocused {
+			if p.pillsExpanded {
 				style = style.PaddingLeft(1).MarginLeft(1).BorderLeft(true).BorderStyle(lipgloss.ThickBorder()).BorderForeground(t.GreenDark)
 			} else {
 				style = style.PaddingLeft(4)
@@ -797,12 +803,11 @@ func (p *chatPage) SetSize(width, height int) tea.Cmd {
 		hasIncompleteTodos := hasIncompleteTodos(p.session.Todos)
 		hasQueue := p.promptQueue > 0
 		hasPills := hasIncompleteTodos || hasQueue
-		pillsFocused := p.focusedPane == PanelTypeStatus
 
 		pillsAreaHeight := 0
 		if hasPills {
 			pillsAreaHeight = pillHeightWithBorder + 1 // +1 for padding top
-			if pillsFocused {
+			if p.pillsExpanded {
 				if p.focusedPillSection == PillSectionTodos && hasIncompleteTodos {
 					pillsAreaHeight += len(p.session.Todos)
 				} else if p.focusedPillSection == PillSectionQueue && hasQueue {
@@ -869,54 +874,48 @@ func (p *chatPage) changeFocus() tea.Cmd {
 		return nil
 	}
 
-	hasPills := hasIncompleteTodos(p.session.Todos) || p.promptQueue > 0
-	wasPillsFocused := p.focusedPane == PanelTypeStatus
-
-	// Focus flow: editor -> status -> chat -> status -> editor (when status exists)
-	// Or: editor -> chat -> editor (when no status)
 	switch p.focusedPane {
 	case PanelTypeEditor:
-		if hasPills {
-			p.previousFocusedPane = PanelTypeEditor
-			p.focusedPane = PanelTypeStatus
-			p.editor.Blur()
-			if hasIncompleteTodos(p.session.Todos) {
-				p.focusedPillSection = PillSectionTodos
-			} else {
-				p.focusedPillSection = PillSectionQueue
-			}
-		} else {
-			p.focusedPane = PanelTypeChat
-			p.chat.Focus()
-			p.editor.Blur()
-		}
-	case PanelTypeStatus:
-		if p.previousFocusedPane == PanelTypeEditor {
-			p.focusedPane = PanelTypeChat
-			p.chat.Focus()
-		} else {
-			p.focusedPane = PanelTypeEditor
-			p.editor.Focus()
-		}
+		p.focusedPane = PanelTypeChat
+		p.chat.Focus()
+		p.editor.Blur()
 	case PanelTypeChat:
-		if hasPills {
-			p.previousFocusedPane = PanelTypeChat
-			p.focusedPane = PanelTypeStatus
-			p.chat.Blur()
-			if hasIncompleteTodos(p.session.Todos) {
-				p.focusedPillSection = PillSectionTodos
-			} else {
-				p.focusedPillSection = PillSectionQueue
-			}
+		p.focusedPane = PanelTypeEditor
+		p.editor.Focus()
+		p.chat.Blur()
+	}
+	return nil
+}
+
+func (p *chatPage) togglePillsExpanded() tea.Cmd {
+	hasPills := hasIncompleteTodos(p.session.Todos) || p.promptQueue > 0
+	if !hasPills {
+		return nil
+	}
+	p.pillsExpanded = !p.pillsExpanded
+	if p.pillsExpanded {
+		if hasIncompleteTodos(p.session.Todos) {
+			p.focusedPillSection = PillSectionTodos
 		} else {
-			p.focusedPane = PanelTypeEditor
-			p.editor.Focus()
-			p.chat.Blur()
+			p.focusedPillSection = PillSectionQueue
 		}
 	}
+	return p.SetSize(p.width, p.height)
+}
+
+func (p *chatPage) switchPillSection(dir int) tea.Cmd {
+	if !p.pillsExpanded {
+		return nil
+	}
+	hasIncompleteTodos := hasIncompleteTodos(p.session.Todos)
+	hasQueue := p.promptQueue > 0
 
-	isPillsFocused := p.focusedPane == PanelTypeStatus
-	if hasPills && (wasPillsFocused != isPillsFocused) {
+	if dir < 0 && p.focusedPillSection == PillSectionQueue && hasIncompleteTodos {
+		p.focusedPillSection = PillSectionTodos
+		return p.SetSize(p.width, p.height)
+	}
+	if dir > 0 && p.focusedPillSection == PillSectionTodos && hasQueue {
+		p.focusedPillSection = PillSectionQueue
 		return p.SetSize(p.width, p.height)
 	}
 	return nil
@@ -987,33 +986,6 @@ func (p *chatPage) sendMessage(text string, attachments []message.Attachment) te
 	return tea.Batch(cmds...)
 }
 
-func (p *chatPage) handlePillsKeyPress(msg tea.KeyPressMsg) tea.Cmd {
-	hasIncompleteTodos := hasIncompleteTodos(p.session.Todos)
-	hasQueue := p.promptQueue > 0
-
-	switch {
-	case key.Matches(msg, p.pillsKeyMap.Left):
-		if p.focusedPillSection == PillSectionQueue && hasIncompleteTodos {
-			p.focusedPillSection = PillSectionTodos
-			return p.SetSize(p.width, p.height)
-		}
-	case key.Matches(msg, p.pillsKeyMap.Right):
-		if p.focusedPillSection == PillSectionTodos && hasQueue {
-			p.focusedPillSection = PillSectionQueue
-			return p.SetSize(p.width, p.height)
-		}
-	case key.Matches(msg, p.pillsKeyMap.Up):
-		p.focusedPane = PanelTypeChat
-		p.chat.Focus()
-		return p.SetSize(p.width, p.height)
-	case key.Matches(msg, p.pillsKeyMap.Down):
-		p.focusedPane = PanelTypeEditor
-		p.editor.Focus()
-		return p.SetSize(p.width, p.height)
-	}
-	return nil
-}
-
 func (p *chatPage) Bindings() []key.Binding {
 	bindings := []key.Binding{
 		p.keyMap.NewSession,
@@ -1235,55 +1207,15 @@ func (p *chatPage) Help() help.KeyMap {
 			var tabKey key.Binding
 			switch p.focusedPane {
 			case PanelTypeEditor:
-				hasIncompleteTodos := hasIncompleteTodos(p.session.Todos)
-				hasQueue := p.promptQueue > 0
-				if hasIncompleteTodos {
-					tabKey = key.NewBinding(
-						key.WithKeys("tab"),
-						key.WithHelp("tab", "focus todos"),
-					)
-				} else if hasQueue {
-					tabKey = key.NewBinding(
-						key.WithKeys("tab"),
-						key.WithHelp("tab", "focus queued"),
-					)
-				} else {
-					tabKey = key.NewBinding(
-						key.WithKeys("tab"),
-						key.WithHelp("tab", "focus chat"),
-					)
-				}
-			case PanelTypeStatus:
-				if p.previousFocusedPane == PanelTypeEditor {
-					tabKey = key.NewBinding(
-						key.WithKeys("tab"),
-						key.WithHelp("tab", "focus chat"),
-					)
-				} else {
-					tabKey = key.NewBinding(
-						key.WithKeys("tab"),
-						key.WithHelp("tab", "focus editor"),
-					)
-				}
+				tabKey = key.NewBinding(
+					key.WithKeys("tab"),
+					key.WithHelp("tab", "focus chat"),
+				)
 			case PanelTypeChat:
-				hasIncompleteTodos := hasIncompleteTodos(p.session.Todos)
-				hasQueue := p.promptQueue > 0
-				if hasIncompleteTodos {
-					tabKey = key.NewBinding(
-						key.WithKeys("tab"),
-						key.WithHelp("tab", "focus todos"),
-					)
-				} else if hasQueue {
-					tabKey = key.NewBinding(
-						key.WithKeys("tab"),
-						key.WithHelp("tab", "focus queued"),
-					)
-				} else {
-					tabKey = key.NewBinding(
-						key.WithKeys("tab"),
-						key.WithHelp("tab", "focus editor"),
-					)
-				}
+				tabKey = key.NewBinding(
+					key.WithKeys("tab"),
+					key.WithHelp("tab", "focus editor"),
+				)
 			default:
 				tabKey = key.NewBinding(
 					key.WithKeys("tab"),
@@ -1292,6 +1224,25 @@ func (p *chatPage) Help() help.KeyMap {
 			}
 			shortList = append(shortList, tabKey)
 			globalBindings = append(globalBindings, tabKey)
+
+			// Add toggle pills binding if there are pills
+			hasTodos := hasIncompleteTodos(p.session.Todos)
+			hasQueue := p.promptQueue > 0
+			if hasTodos || hasQueue {
+				toggleBinding := p.keyMap.TogglePills
+				if hasTodos {
+					toggleBinding.SetHelp("ctrl+space", "toggle todos")
+				} else {
+					toggleBinding.SetHelp("ctrl+space", "toggle queued")
+				}
+				shortList = append(shortList, toggleBinding)
+				globalBindings = append(globalBindings, toggleBinding)
+				// Show left/right to switch sections when expanded and both exist
+				if p.pillsExpanded && hasTodos && hasQueue {
+					shortList = append(shortList, p.keyMap.PillLeft)
+					globalBindings = append(globalBindings, p.keyMap.PillLeft)
+				}
+			}
 		}
 		commandsBinding := key.NewBinding(
 			key.WithKeys("ctrl+p"),
@@ -1430,15 +1381,6 @@ func (p *chatPage) Help() help.KeyMap {
 					),
 				})
 			}
-		case PanelTypeStatus:
-			shortList = append(shortList,
-				p.pillsKeyMap.Left,
-			)
-			fullList = append(fullList,
-				[]key.Binding{
-					p.pillsKeyMap.Left,
-				},
-			)
 		}
 		shortList = append(shortList,
 			// Quit

internal/tui/page/chat/keys.go 🔗

@@ -10,13 +10,9 @@ type KeyMap struct {
 	Cancel        key.Binding
 	Tab           key.Binding
 	Details       key.Binding
-}
-
-type PillsKeyMap struct {
-	Left  key.Binding
-	Right key.Binding
-	Up    key.Binding
-	Down  key.Binding
+	TogglePills   key.Binding
+	PillLeft      key.Binding
+	PillRight     key.Binding
 }
 
 func DefaultKeyMap() KeyMap {
@@ -41,26 +37,17 @@ func DefaultKeyMap() KeyMap {
 			key.WithKeys("ctrl+d"),
 			key.WithHelp("ctrl+d", "toggle details"),
 		),
-	}
-}
-
-func DefaultPillsKeyMap() PillsKeyMap {
-	return PillsKeyMap{
-		Left: key.NewBinding(
+		TogglePills: key.NewBinding(
+			key.WithKeys("ctrl+space"),
+			key.WithHelp("ctrl+space", "toggle tasks"),
+		),
+		PillLeft: key.NewBinding(
 			key.WithKeys("left"),
-			key.WithHelp("←/→", "section"),
+			key.WithHelp("←/→", "switch section"),
 		),
-		Right: key.NewBinding(
+		PillRight: key.NewBinding(
 			key.WithKeys("right"),
-			key.WithHelp("←/→", "section"),
-		),
-		Up: key.NewBinding(
-			key.WithKeys("up", "k"),
-			key.WithHelp("↑/k", "focus chat"),
-		),
-		Down: key.NewBinding(
-			key.WithKeys("down", "j"),
-			key.WithHelp("↓/j", "focus pills"),
+			key.WithHelp("←/→", "switch section"),
 		),
 	}
 }