session.go 5.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233
  1. package dialog
  2. import (
  3. "github.com/charmbracelet/bubbles/key"
  4. tea "github.com/charmbracelet/bubbletea"
  5. "github.com/charmbracelet/lipgloss"
  6. "github.com/opencode-ai/opencode/internal/session"
  7. "github.com/opencode-ai/opencode/internal/tui/layout"
  8. "github.com/opencode-ai/opencode/internal/tui/styles"
  9. "github.com/opencode-ai/opencode/internal/tui/theme"
  10. "github.com/opencode-ai/opencode/internal/tui/util"
  11. )
  12. // SessionSelectedMsg is sent when a session is selected
  13. type SessionSelectedMsg struct {
  14. Session session.Session
  15. }
  16. // CloseSessionDialogMsg is sent when the session dialog is closed
  17. type CloseSessionDialogMsg struct{}
  18. // SessionDialog interface for the session switching dialog
  19. type SessionDialog interface {
  20. tea.Model
  21. layout.Bindings
  22. SetSessions(sessions []session.Session)
  23. SetSelectedSession(sessionID string)
  24. }
  25. type sessionDialogCmp struct {
  26. sessions []session.Session
  27. selectedIdx int
  28. width int
  29. height int
  30. selectedSessionID string
  31. }
  32. type sessionKeyMap struct {
  33. Up key.Binding
  34. Down key.Binding
  35. Enter key.Binding
  36. Escape key.Binding
  37. J key.Binding
  38. K key.Binding
  39. }
  40. var sessionKeys = sessionKeyMap{
  41. Up: key.NewBinding(
  42. key.WithKeys("up"),
  43. key.WithHelp("↑", "previous session"),
  44. ),
  45. Down: key.NewBinding(
  46. key.WithKeys("down"),
  47. key.WithHelp("↓", "next session"),
  48. ),
  49. Enter: key.NewBinding(
  50. key.WithKeys("enter"),
  51. key.WithHelp("enter", "select session"),
  52. ),
  53. Escape: key.NewBinding(
  54. key.WithKeys("esc"),
  55. key.WithHelp("esc", "close"),
  56. ),
  57. J: key.NewBinding(
  58. key.WithKeys("j"),
  59. key.WithHelp("j", "next session"),
  60. ),
  61. K: key.NewBinding(
  62. key.WithKeys("k"),
  63. key.WithHelp("k", "previous session"),
  64. ),
  65. }
  66. func (s *sessionDialogCmp) Init() tea.Cmd {
  67. return nil
  68. }
  69. func (s *sessionDialogCmp) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
  70. switch msg := msg.(type) {
  71. case tea.KeyMsg:
  72. switch {
  73. case key.Matches(msg, sessionKeys.Up) || key.Matches(msg, sessionKeys.K):
  74. if s.selectedIdx > 0 {
  75. s.selectedIdx--
  76. }
  77. return s, nil
  78. case key.Matches(msg, sessionKeys.Down) || key.Matches(msg, sessionKeys.J):
  79. if s.selectedIdx < len(s.sessions)-1 {
  80. s.selectedIdx++
  81. }
  82. return s, nil
  83. case key.Matches(msg, sessionKeys.Enter):
  84. if len(s.sessions) > 0 {
  85. selectedSession := s.sessions[s.selectedIdx]
  86. // Update the session manager with the selected session
  87. session.SetCurrentSession(selectedSession.ID)
  88. return s, util.CmdHandler(SessionSelectedMsg{
  89. Session: selectedSession,
  90. })
  91. }
  92. case key.Matches(msg, sessionKeys.Escape):
  93. return s, util.CmdHandler(CloseSessionDialogMsg{})
  94. }
  95. case tea.WindowSizeMsg:
  96. s.width = msg.Width
  97. s.height = msg.Height
  98. }
  99. return s, nil
  100. }
  101. func (s *sessionDialogCmp) View() string {
  102. t := theme.CurrentTheme()
  103. baseStyle := styles.BaseStyle()
  104. if len(s.sessions) == 0 {
  105. return baseStyle.Padding(1, 2).
  106. Border(lipgloss.RoundedBorder()).
  107. BorderBackground(t.Background()).
  108. BorderForeground(t.TextMuted()).
  109. Width(40).
  110. Render("No sessions available")
  111. }
  112. // Calculate max width needed for session titles
  113. maxWidth := 40 // Minimum width
  114. for _, sess := range s.sessions {
  115. if len(sess.Title) > maxWidth-4 { // Account for padding
  116. maxWidth = len(sess.Title) + 4
  117. }
  118. }
  119. maxWidth = max(30, min(maxWidth, s.width-15)) // Limit width to avoid overflow
  120. // Limit height to avoid taking up too much screen space
  121. maxVisibleSessions := min(10, len(s.sessions))
  122. // Build the session list
  123. sessionItems := make([]string, 0, maxVisibleSessions)
  124. startIdx := 0
  125. // If we have more sessions than can be displayed, adjust the start index
  126. if len(s.sessions) > maxVisibleSessions {
  127. // Center the selected item when possible
  128. halfVisible := maxVisibleSessions / 2
  129. if s.selectedIdx >= halfVisible && s.selectedIdx < len(s.sessions)-halfVisible {
  130. startIdx = s.selectedIdx - halfVisible
  131. } else if s.selectedIdx >= len(s.sessions)-halfVisible {
  132. startIdx = len(s.sessions) - maxVisibleSessions
  133. }
  134. }
  135. endIdx := min(startIdx+maxVisibleSessions, len(s.sessions))
  136. for i := startIdx; i < endIdx; i++ {
  137. sess := s.sessions[i]
  138. itemStyle := baseStyle.Width(maxWidth)
  139. if i == s.selectedIdx {
  140. itemStyle = itemStyle.
  141. Background(t.Primary()).
  142. Foreground(t.Background()).
  143. Bold(true)
  144. }
  145. sessionItems = append(sessionItems, itemStyle.Padding(0, 1).Render(sess.Title))
  146. }
  147. title := baseStyle.
  148. Foreground(t.Primary()).
  149. Bold(true).
  150. Width(maxWidth).
  151. Padding(0, 1).
  152. Render("Switch Session")
  153. content := lipgloss.JoinVertical(
  154. lipgloss.Left,
  155. title,
  156. baseStyle.Width(maxWidth).Render(""),
  157. baseStyle.Width(maxWidth).Render(lipgloss.JoinVertical(lipgloss.Left, sessionItems...)),
  158. baseStyle.Width(maxWidth).Render(""),
  159. )
  160. return baseStyle.Padding(1, 2).
  161. Border(lipgloss.RoundedBorder()).
  162. BorderBackground(t.Background()).
  163. BorderForeground(t.TextMuted()).
  164. Width(lipgloss.Width(content) + 4).
  165. Render(content)
  166. }
  167. func (s *sessionDialogCmp) BindingKeys() []key.Binding {
  168. return layout.KeyMapToSlice(sessionKeys)
  169. }
  170. func (s *sessionDialogCmp) SetSessions(sessions []session.Session) {
  171. s.sessions = sessions
  172. // If we have a selected session ID, find its index
  173. if s.selectedSessionID != "" {
  174. for i, sess := range sessions {
  175. if sess.ID == s.selectedSessionID {
  176. s.selectedIdx = i
  177. return
  178. }
  179. }
  180. }
  181. // Default to first session if selected not found
  182. s.selectedIdx = 0
  183. }
  184. func (s *sessionDialogCmp) SetSelectedSession(sessionID string) {
  185. s.selectedSessionID = sessionID
  186. // Update the selected index if sessions are already loaded
  187. if len(s.sessions) > 0 {
  188. for i, sess := range s.sessions {
  189. if sess.ID == sessionID {
  190. s.selectedIdx = i
  191. return
  192. }
  193. }
  194. }
  195. }
  196. // NewSessionDialogCmp creates a new session switching dialog
  197. func NewSessionDialogCmp() SessionDialog {
  198. return &sessionDialogCmp{
  199. sessions: []session.Session{},
  200. selectedIdx: 0,
  201. selectedSessionID: "",
  202. }
  203. }