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