session.go 5.6 KB

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