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}