feat(pills): add toggle todos/pills menu item (#2202)

Christian Rocha created

Change summary

internal/session/session.go    | 10 ++++++++++
internal/ui/dialog/actions.go  |  1 +
internal/ui/dialog/commands.go | 35 ++++++++++++++++++++++++++++-------
internal/ui/model/pills.go     |  7 +------
internal/ui/model/ui.go        | 14 +++++++++++---
5 files changed, 51 insertions(+), 16 deletions(-)

Detailed changes

internal/session/session.go 🔗

@@ -28,6 +28,16 @@ type Todo struct {
 	ActiveForm string     `json:"active_form"`
 }
 
+// HasIncompleteTodos returns true if there are any non-completed todos.
+func HasIncompleteTodos(todos []Todo) bool {
+	for _, todo := range todos {
+		if todo.Status != TodoStatusCompleted {
+			return true
+		}
+	}
+	return false
+}
+
 type Session struct {
 	ID               string
 	ParentSessionID  string

internal/ui/dialog/actions.go 🔗

@@ -48,6 +48,7 @@ type (
 	ActionToggleHelp        struct{}
 	ActionToggleCompactMode struct{}
 	ActionToggleThinking    struct{}
+	ActionTogglePills       struct{}
 	ActionExternalEditor    struct{}
 	ActionToggleYoloMode    struct{}
 	// ActionInitializeProject is a message to initialize a project.

internal/ui/dialog/commands.go 🔗

@@ -49,8 +49,11 @@ type Commands struct {
 		Close key.Binding
 	}
 
-	sessionID string // can be empty for non-session-specific commands
-	selected  CommandType
+	sessionID  string
+	hasSession bool
+	hasTodos   bool
+	hasQueue   bool
+	selected   CommandType
 
 	spinner spinner.Model
 	loading bool
@@ -68,11 +71,14 @@ type Commands struct {
 var _ Dialog = (*Commands)(nil)
 
 // NewCommands creates a new commands dialog.
-func NewCommands(com *common.Common, sessionID string, customCommands []commands.CustomCommand, mcpPrompts []commands.MCPPrompt) (*Commands, error) {
+func NewCommands(com *common.Common, sessionID string, hasSession, hasTodos, hasQueue bool, customCommands []commands.CustomCommand, mcpPrompts []commands.MCPPrompt) (*Commands, error) {
 	c := &Commands{
 		com:            com,
 		selected:       SystemCommands,
 		sessionID:      sessionID,
+		hasSession:     hasSession,
+		hasTodos:       hasTodos,
+		hasQueue:       hasQueue,
 		customCommands: customCommands,
 		mcpPrompts:     mcpPrompts,
 	}
@@ -387,7 +393,7 @@ func (c *Commands) defaultCommands() []*CommandItem {
 	}
 
 	// Only show compact command if there's an active session
-	if c.sessionID != "" {
+	if c.hasSession {
 		commands = append(commands, NewCommandItem(c.com.Styles, "summarize", "Summarize Session", "", ActionSummarize{SessionID: c.sessionID}))
 	}
 
@@ -417,10 +423,10 @@ func (c *Commands) defaultCommands() []*CommandItem {
 		}
 	}
 	// Only show toggle compact mode command if window width is larger than compact breakpoint (120)
-	if c.windowWidth >= sidebarCompactModeBreakpoint && c.sessionID != "" {
+	if c.windowWidth >= sidebarCompactModeBreakpoint && c.hasSession {
 		commands = append(commands, NewCommandItem(c.com.Styles, "toggle_sidebar", "Toggle Sidebar", "", ActionToggleCompactMode{}))
 	}
-	if c.sessionID != "" {
+	if c.hasSession {
 		cfg := c.com.Config()
 		agentCfg := cfg.Agents[config.AgentCoder]
 		model := cfg.GetModelByType(agentCfg.Model)
@@ -437,12 +443,27 @@ func (c *Commands) defaultCommands() []*CommandItem {
 		commands = append(commands, NewCommandItem(c.com.Styles, "open_external_editor", "Open External Editor", "ctrl+o", ActionExternalEditor{}))
 	}
 
-	return append(commands,
+	if c.hasTodos || c.hasQueue {
+		var label string
+		switch {
+		case c.hasTodos && c.hasQueue:
+			label = "Toggle To-Dos/Queue"
+		case c.hasQueue:
+			label = "Toggle Queue"
+		default:
+			label = "Toggle To-Dos"
+		}
+		commands = append(commands, NewCommandItem(c.com.Styles, "toggle_pills", label, "ctrl+t", ActionTogglePills{}))
+	}
+
+	commands = append(commands,
 		NewCommandItem(c.com.Styles, "toggle_yolo", "Toggle Yolo Mode", "", ActionToggleYoloMode{}),
 		NewCommandItem(c.com.Styles, "toggle_help", "Toggle Help", "ctrl+g", ActionToggleHelp{}),
 		NewCommandItem(c.com.Styles, "init", "Initialize Project", "", ActionInitializeProject{}),
 		NewCommandItem(c.com.Styles, "quit", "Quit", "ctrl+c", tea.QuitMsg{}),
 	)
+
+	return commands
 }
 
 // SetCustomCommands sets the custom commands and refreshes the view if user commands are currently displayed.

internal/ui/model/pills.go 🔗

@@ -38,12 +38,7 @@ const (
 
 // hasIncompleteTodos returns true if there are any non-completed todos.
 func hasIncompleteTodos(todos []session.Todo) bool {
-	for _, todo := range todos {
-		if todo.Status != session.TodoStatusCompleted {
-			return true
-		}
-	}
-	return false
+	return session.HasIncompleteTodos(todos)
 }
 
 // hasInProgressTodo returns true if there is at least one in-progress todo.

internal/ui/model/ui.go 🔗

@@ -1213,6 +1213,11 @@ func (m *UI) handleDialogMsg(msg tea.Msg) tea.Cmd {
 	case dialog.ActionToggleCompactMode:
 		cmds = append(cmds, m.toggleCompactMode())
 		m.dialog.CloseDialog(dialog.CommandsID)
+	case dialog.ActionTogglePills:
+		if cmd := m.togglePillsExpanded(); cmd != nil {
+			cmds = append(cmds, cmd)
+		}
+		m.dialog.CloseDialog(dialog.CommandsID)
 	case dialog.ActionToggleThinking:
 		cmds = append(cmds, func() tea.Msg {
 			cfg := m.com.Config()
@@ -2858,12 +2863,15 @@ func (m *UI) openCommandsDialog() tea.Cmd {
 		return nil
 	}
 
-	sessionID := ""
-	if m.session != nil {
+	var sessionID string
+	hasSession := m.session != nil
+	if hasSession {
 		sessionID = m.session.ID
 	}
+	hasTodos := hasSession && hasIncompleteTodos(m.session.Todos)
+	hasQueue := m.promptQueue > 0
 
-	commands, err := dialog.NewCommands(m.com, sessionID, m.customCommands, m.mcpPrompts)
+	commands, err := dialog.NewCommands(m.com, sessionID, hasSession, hasTodos, hasQueue, m.customCommands, m.mcpPrompts)
 	if err != nil {
 		return util.ReportError(err)
 	}