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	maxWidth = max(30, min(maxWidth, s.width-15)) // Limit width to avoid overflow
126
127	// Limit height to avoid taking up too much screen space
128	maxVisibleSessions := min(10, len(s.sessions))
129
130	// Build the session list
131	sessionItems := make([]string, 0, maxVisibleSessions)
132	startIdx := 0
133
134	// If we have more sessions than can be displayed, adjust the start index
135	if len(s.sessions) > maxVisibleSessions {
136		// Center the selected item when possible
137		halfVisible := maxVisibleSessions / 2
138		if s.selectedIdx >= halfVisible && s.selectedIdx < len(s.sessions)-halfVisible {
139			startIdx = s.selectedIdx - halfVisible
140		} else if s.selectedIdx >= len(s.sessions)-halfVisible {
141			startIdx = len(s.sessions) - maxVisibleSessions
142		}
143	}
144
145	endIdx := min(startIdx+maxVisibleSessions, len(s.sessions))
146
147	for i := startIdx; i < endIdx; i++ {
148		sess := s.sessions[i]
149		itemStyle := styles.BaseStyle.Width(maxWidth)
150
151		if i == s.selectedIdx {
152			itemStyle = itemStyle.
153				Background(styles.PrimaryColor).
154				Foreground(styles.Background).
155				Bold(true)
156		}
157
158		sessionItems = append(sessionItems, itemStyle.Padding(0, 1).Render(sess.Title))
159	}
160
161	title := styles.BaseStyle.
162		Foreground(styles.PrimaryColor).
163		Bold(true).
164		Width(maxWidth).
165		Padding(0, 1).
166		Render("Switch Session")
167
168	content := lipgloss.JoinVertical(
169		lipgloss.Left,
170		title,
171		styles.BaseStyle.Width(maxWidth).Render(""),
172		styles.BaseStyle.Width(maxWidth).Render(lipgloss.JoinVertical(lipgloss.Left, sessionItems...)),
173		styles.BaseStyle.Width(maxWidth).Render(""),
174	)
175
176	return styles.BaseStyle.Padding(1, 2).
177		Border(lipgloss.RoundedBorder()).
178		BorderBackground(styles.Background).
179		BorderForeground(styles.ForgroundDim).
180		Width(lipgloss.Width(content) + 4).
181		Render(content)
182}
183
184func (s *sessionDialogCmp) BindingKeys() []key.Binding {
185	return layout.KeyMapToSlice(sessionKeys)
186}
187
188func (s *sessionDialogCmp) SetSessions(sessions []session.Session) {
189	s.sessions = sessions
190
191	// If we have a selected session ID, find its index
192	if s.selectedSessionID != "" {
193		for i, sess := range sessions {
194			if sess.ID == s.selectedSessionID {
195				s.selectedIdx = i
196				return
197			}
198		}
199	}
200
201	// Default to first session if selected not found
202	s.selectedIdx = 0
203}
204
205func (s *sessionDialogCmp) SetSelectedSession(sessionID string) {
206	s.selectedSessionID = sessionID
207
208	// Update the selected index if sessions are already loaded
209	if len(s.sessions) > 0 {
210		for i, sess := range s.sessions {
211			if sess.ID == sessionID {
212				s.selectedIdx = i
213				return
214			}
215		}
216	}
217}
218
219// NewSessionDialogCmp creates a new session switching dialog
220func NewSessionDialogCmp() SessionDialog {
221	return &sessionDialogCmp{
222		sessions:          []session.Session{},
223		selectedIdx:       0,
224		selectedSessionID: "",
225	}
226}