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