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}