session.go

  1package dialog
  2
  3import (
  4	"github.com/charmbracelet/bubbles/key"
  5	tea "github.com/charmbracelet/bubbletea"
  6	"github.com/charmbracelet/lipgloss"
  7	"github.com/kujtimiihoxha/opencode/internal/session"
  8	"github.com/kujtimiihoxha/opencode/internal/tui/layout"
  9	"github.com/kujtimiihoxha/opencode/internal/tui/styles"
 10	"github.com/kujtimiihoxha/opencode/internal/tui/util"
 11)
 12
 13// SessionSelectedMsg is sent when a session is selected
 14type SessionSelectedMsg struct {
 15	Session session.Session
 16}
 17
 18// CloseSessionDialogMsg is sent when the session dialog is closed
 19type CloseSessionDialogMsg struct{}
 20
 21// SessionDialog interface for the session switching dialog
 22type SessionDialog interface {
 23	tea.Model
 24	layout.Bindings
 25	SetSessions(sessions []session.Session)
 26	SetSelectedSession(sessionID string)
 27}
 28
 29type sessionDialogCmp struct {
 30	sessions     []session.Session
 31	selectedIdx  int
 32	width        int
 33	height       int
 34	selectedSessionID string
 35}
 36
 37type sessionKeyMap struct {
 38	Up         key.Binding
 39	Down       key.Binding
 40	Enter      key.Binding
 41	Escape     key.Binding
 42	J          key.Binding
 43	K          key.Binding
 44}
 45
 46var sessionKeys = sessionKeyMap{
 47	Up: key.NewBinding(
 48		key.WithKeys("up"),
 49		key.WithHelp("↑", "previous session"),
 50	),
 51	Down: key.NewBinding(
 52		key.WithKeys("down"),
 53		key.WithHelp("↓", "next session"),
 54	),
 55	Enter: key.NewBinding(
 56		key.WithKeys("enter"),
 57		key.WithHelp("enter", "select session"),
 58	),
 59	Escape: key.NewBinding(
 60		key.WithKeys("esc"),
 61		key.WithHelp("esc", "close"),
 62	),
 63	J: key.NewBinding(
 64		key.WithKeys("j"),
 65		key.WithHelp("j", "next session"),
 66	),
 67	K: key.NewBinding(
 68		key.WithKeys("k"),
 69		key.WithHelp("k", "previous session"),
 70	),
 71}
 72
 73func (s *sessionDialogCmp) Init() tea.Cmd {
 74	return nil
 75}
 76
 77func (s *sessionDialogCmp) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
 78	switch msg := msg.(type) {
 79	case tea.KeyMsg:
 80		switch {
 81		case key.Matches(msg, sessionKeys.Up) || key.Matches(msg, sessionKeys.K):
 82			if s.selectedIdx > 0 {
 83				s.selectedIdx--
 84			}
 85			return s, nil
 86		case key.Matches(msg, sessionKeys.Down) || key.Matches(msg, sessionKeys.J):
 87			if s.selectedIdx < len(s.sessions)-1 {
 88				s.selectedIdx++
 89			}
 90			return s, nil
 91		case key.Matches(msg, sessionKeys.Enter):
 92			if len(s.sessions) > 0 {
 93				return s, util.CmdHandler(SessionSelectedMsg{
 94					Session: s.sessions[s.selectedIdx],
 95				})
 96			}
 97		case key.Matches(msg, sessionKeys.Escape):
 98			return s, util.CmdHandler(CloseSessionDialogMsg{})
 99		}
100	case tea.WindowSizeMsg:
101		s.width = msg.Width
102		s.height = msg.Height
103	}
104	return s, nil
105}
106
107func (s *sessionDialogCmp) View() string {
108	if len(s.sessions) == 0 {
109		return styles.BaseStyle.Padding(1, 2).
110			Border(lipgloss.RoundedBorder()).
111			BorderBackground(styles.Background).
112			BorderForeground(styles.ForgroundDim).
113			Width(40).
114			Render("No sessions available")
115	}
116
117	// Calculate max width needed for session titles
118	maxWidth := 40 // Minimum width
119	for _, sess := range s.sessions {
120		if len(sess.Title) > maxWidth-4 { // Account for padding
121			maxWidth = len(sess.Title) + 4
122		}
123	}
124
125	// Limit height to avoid taking up too much screen space
126	maxVisibleSessions := min(10, len(s.sessions))
127
128	// Build the session list
129	sessionItems := make([]string, 0, maxVisibleSessions)
130	startIdx := 0
131	
132	// If we have more sessions than can be displayed, adjust the start index
133	if len(s.sessions) > maxVisibleSessions {
134		// Center the selected item when possible
135		halfVisible := maxVisibleSessions / 2
136		if s.selectedIdx >= halfVisible && s.selectedIdx < len(s.sessions)-halfVisible {
137			startIdx = s.selectedIdx - halfVisible
138		} else if s.selectedIdx >= len(s.sessions)-halfVisible {
139			startIdx = len(s.sessions) - maxVisibleSessions
140		}
141	}
142
143	endIdx := min(startIdx+maxVisibleSessions, len(s.sessions))
144
145	for i := startIdx; i < endIdx; i++ {
146		sess := s.sessions[i]
147		itemStyle := styles.BaseStyle.Width(maxWidth)
148		
149		if i == s.selectedIdx {
150			itemStyle = itemStyle.
151				Background(styles.PrimaryColor).
152				Foreground(styles.Background).
153				Bold(true)
154		}
155		
156		sessionItems = append(sessionItems, itemStyle.Padding(0, 1).Render(sess.Title))
157	}
158
159	title := styles.BaseStyle.
160		Foreground(styles.PrimaryColor).
161		Bold(true).
162		Padding(0, 1).
163		Render("Switch Session")
164
165	content := lipgloss.JoinVertical(
166		lipgloss.Left,
167		title,
168		styles.BaseStyle.Render(""),
169		lipgloss.JoinVertical(lipgloss.Left, sessionItems...),
170		styles.BaseStyle.Render(""),
171		styles.BaseStyle.Foreground(styles.ForgroundDim).Render("↑/k: up  ↓/j: down  enter: select  esc: cancel"),
172	)
173
174	return styles.BaseStyle.Padding(1, 2).
175		Border(lipgloss.RoundedBorder()).
176		BorderBackground(styles.Background).
177		BorderForeground(styles.ForgroundDim).
178		Width(lipgloss.Width(content) + 4).
179		Render(content)
180}
181
182func (s *sessionDialogCmp) BindingKeys() []key.Binding {
183	return layout.KeyMapToSlice(sessionKeys)
184}
185
186func (s *sessionDialogCmp) SetSessions(sessions []session.Session) {
187	s.sessions = sessions
188	
189	// If we have a selected session ID, find its index
190	if s.selectedSessionID != "" {
191		for i, sess := range sessions {
192			if sess.ID == s.selectedSessionID {
193				s.selectedIdx = i
194				return
195			}
196		}
197	}
198	
199	// Default to first session if selected not found
200	s.selectedIdx = 0
201}
202
203func (s *sessionDialogCmp) SetSelectedSession(sessionID string) {
204	s.selectedSessionID = sessionID
205	
206	// Update the selected index if sessions are already loaded
207	if len(s.sessions) > 0 {
208		for i, sess := range s.sessions {
209			if sess.ID == sessionID {
210				s.selectedIdx = i
211				return
212			}
213		}
214	}
215}
216
217// NewSessionDialogCmp creates a new session switching dialog
218func NewSessionDialogCmp() SessionDialog {
219	return &sessionDialogCmp{
220		sessions:         []session.Session{},
221		selectedIdx:      0,
222		selectedSessionID: "",
223	}
224}