session.go 5.6 KB

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