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