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 Padding(0, 1).
163 Render("Switch Session")
164
165 content := lipgloss.JoinVertical(
166 lipgloss.Left,
167 title,
168 styles.BaseStyle.Render(""),
169 lipgloss.JoinVertical(lipgloss.Left, sessionItems...),
170 styles.BaseStyle.Render(""),
171 styles.BaseStyle.Foreground(styles.ForgroundDim).Render("↑/k: up ↓/j: down enter: select esc: cancel"),
172 )
173
174 return styles.BaseStyle.Padding(1, 2).
175 Border(lipgloss.RoundedBorder()).
176 BorderBackground(styles.Background).
177 BorderForeground(styles.ForgroundDim).
178 Width(lipgloss.Width(content) + 4).
179 Render(content)
180}
181
182func (s *sessionDialogCmp) BindingKeys() []key.Binding {
183 return layout.KeyMapToSlice(sessionKeys)
184}
185
186func (s *sessionDialogCmp) SetSessions(sessions []session.Session) {
187 s.sessions = sessions
188
189 // If we have a selected session ID, find its index
190 if s.selectedSessionID != "" {
191 for i, sess := range sessions {
192 if sess.ID == s.selectedSessionID {
193 s.selectedIdx = i
194 return
195 }
196 }
197 }
198
199 // Default to first session if selected not found
200 s.selectedIdx = 0
201}
202
203func (s *sessionDialogCmp) SetSelectedSession(sessionID string) {
204 s.selectedSessionID = sessionID
205
206 // Update the selected index if sessions are already loaded
207 if len(s.sessions) > 0 {
208 for i, sess := range s.sessions {
209 if sess.ID == sessionID {
210 s.selectedIdx = i
211 return
212 }
213 }
214 }
215}
216
217// NewSessionDialogCmp creates a new session switching dialog
218func NewSessionDialogCmp() SessionDialog {
219 return &sessionDialogCmp{
220 sessions: []session.Session{},
221 selectedIdx: 0,
222 selectedSessionID: "",
223 }
224}