| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230 |
- package dialog
- import (
- "github.com/charmbracelet/bubbles/key"
- tea "github.com/charmbracelet/bubbletea"
- "github.com/charmbracelet/lipgloss"
- "github.com/sst/opencode/internal/session"
- "github.com/sst/opencode/internal/tui/layout"
- "github.com/sst/opencode/internal/tui/styles"
- "github.com/sst/opencode/internal/tui/theme"
- "github.com/sst/opencode/internal/tui/util"
- )
- // CloseSessionDialogMsg is sent when the session dialog is closed
- type CloseSessionDialogMsg struct {
- Session *session.Session
- }
- // SessionDialog interface for the session switching dialog
- type SessionDialog interface {
- tea.Model
- layout.Bindings
- SetSessions(sessions []session.Session)
- SetSelectedSession(sessionID string)
- }
- type sessionDialogCmp struct {
- sessions []session.Session
- selectedIdx int
- width int
- height int
- selectedSessionID string
- }
- type sessionKeyMap struct {
- Up key.Binding
- Down key.Binding
- Enter key.Binding
- Escape key.Binding
- J key.Binding
- K key.Binding
- }
- var sessionKeys = sessionKeyMap{
- Up: key.NewBinding(
- key.WithKeys("up"),
- key.WithHelp("↑", "previous session"),
- ),
- Down: key.NewBinding(
- key.WithKeys("down"),
- key.WithHelp("↓", "next session"),
- ),
- Enter: key.NewBinding(
- key.WithKeys("enter"),
- key.WithHelp("enter", "select session"),
- ),
- Escape: key.NewBinding(
- key.WithKeys("esc"),
- key.WithHelp("esc", "close"),
- ),
- J: key.NewBinding(
- key.WithKeys("j"),
- key.WithHelp("j", "next session"),
- ),
- K: key.NewBinding(
- key.WithKeys("k"),
- key.WithHelp("k", "previous session"),
- ),
- }
- func (s *sessionDialogCmp) Init() tea.Cmd {
- return nil
- }
- func (s *sessionDialogCmp) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
- switch msg := msg.(type) {
- case tea.KeyMsg:
- switch {
- case key.Matches(msg, sessionKeys.Up) || key.Matches(msg, sessionKeys.K):
- if s.selectedIdx > 0 {
- s.selectedIdx--
- }
- return s, nil
- case key.Matches(msg, sessionKeys.Down) || key.Matches(msg, sessionKeys.J):
- if s.selectedIdx < len(s.sessions)-1 {
- s.selectedIdx++
- }
- return s, nil
- case key.Matches(msg, sessionKeys.Enter):
- if len(s.sessions) > 0 {
- selectedSession := s.sessions[s.selectedIdx]
- s.selectedSessionID = selectedSession.ID
- return s, util.CmdHandler(CloseSessionDialogMsg{
- Session: &selectedSession,
- })
- }
- case key.Matches(msg, sessionKeys.Escape):
- return s, util.CmdHandler(CloseSessionDialogMsg{})
- }
- case tea.WindowSizeMsg:
- s.width = msg.Width
- s.height = msg.Height
- }
- return s, nil
- }
- func (s *sessionDialogCmp) View() string {
- t := theme.CurrentTheme()
- baseStyle := styles.BaseStyle()
- if len(s.sessions) == 0 {
- return baseStyle.Padding(1, 2).
- Border(lipgloss.RoundedBorder()).
- BorderBackground(t.Background()).
- BorderForeground(t.TextMuted()).
- Width(40).
- Render("No sessions available")
- }
- // Calculate max width needed for session titles
- maxWidth := 40 // Minimum width
- for _, sess := range s.sessions {
- if len(sess.Title) > maxWidth-4 { // Account for padding
- maxWidth = len(sess.Title) + 4
- }
- }
- maxWidth = max(30, min(maxWidth, s.width-15)) // Limit width to avoid overflow
- // Limit height to avoid taking up too much screen space
- maxVisibleSessions := min(10, len(s.sessions))
- // Build the session list
- sessionItems := make([]string, 0, maxVisibleSessions)
- startIdx := 0
- // If we have more sessions than can be displayed, adjust the start index
- if len(s.sessions) > maxVisibleSessions {
- // Center the selected item when possible
- halfVisible := maxVisibleSessions / 2
- if s.selectedIdx >= halfVisible && s.selectedIdx < len(s.sessions)-halfVisible {
- startIdx = s.selectedIdx - halfVisible
- } else if s.selectedIdx >= len(s.sessions)-halfVisible {
- startIdx = len(s.sessions) - maxVisibleSessions
- }
- }
- endIdx := min(startIdx+maxVisibleSessions, len(s.sessions))
- for i := startIdx; i < endIdx; i++ {
- sess := s.sessions[i]
- itemStyle := baseStyle.Width(maxWidth)
- if i == s.selectedIdx {
- itemStyle = itemStyle.
- Background(t.Primary()).
- Foreground(t.Background()).
- Bold(true)
- }
- sessionItems = append(sessionItems, itemStyle.Padding(0, 1).Render(sess.Title))
- }
- title := baseStyle.
- Foreground(t.Primary()).
- Bold(true).
- Width(maxWidth).
- Padding(0, 1).
- Render("Switch Session")
- content := lipgloss.JoinVertical(
- lipgloss.Left,
- title,
- baseStyle.Width(maxWidth).Render(""),
- baseStyle.Width(maxWidth).Render(lipgloss.JoinVertical(lipgloss.Left, sessionItems...)),
- baseStyle.Width(maxWidth).Render(""),
- )
- return baseStyle.Padding(1, 2).
- Border(lipgloss.RoundedBorder()).
- BorderBackground(t.Background()).
- BorderForeground(t.TextMuted()).
- Width(lipgloss.Width(content) + 4).
- Render(content)
- }
- func (s *sessionDialogCmp) BindingKeys() []key.Binding {
- return layout.KeyMapToSlice(sessionKeys)
- }
- func (s *sessionDialogCmp) SetSessions(sessions []session.Session) {
- s.sessions = sessions
- // If we have a selected session ID, find its index
- if s.selectedSessionID != "" {
- for i, sess := range sessions {
- if sess.ID == s.selectedSessionID {
- s.selectedIdx = i
- return
- }
- }
- }
- // Default to first session if selected not found
- s.selectedIdx = 0
- }
- func (s *sessionDialogCmp) SetSelectedSession(sessionID string) {
- s.selectedSessionID = sessionID
- // Update the selected index if sessions are already loaded
- if len(s.sessions) > 0 {
- for i, sess := range s.sessions {
- if sess.ID == sessionID {
- s.selectedIdx = i
- return
- }
- }
- }
- }
- // NewSessionDialogCmp creates a new session switching dialog
- func NewSessionDialogCmp() SessionDialog {
- return &sessionDialogCmp{
- sessions: []session.Session{},
- selectedIdx: 0,
- selectedSessionID: "",
- }
- }
|