sessions.go

  1package sessions
  2
  3import (
  4	"github.com/charmbracelet/bubbles/v2/help"
  5	"github.com/charmbracelet/bubbles/v2/key"
  6	tea "github.com/charmbracelet/bubbletea/v2"
  7	"github.com/charmbracelet/lipgloss/v2"
  8	"github.com/opencode-ai/opencode/internal/session"
  9	"github.com/opencode-ai/opencode/internal/tui/components/chat"
 10	"github.com/opencode-ai/opencode/internal/tui/components/completions"
 11	"github.com/opencode-ai/opencode/internal/tui/components/core"
 12	"github.com/opencode-ai/opencode/internal/tui/components/core/list"
 13	"github.com/opencode-ai/opencode/internal/tui/components/dialogs"
 14	"github.com/opencode-ai/opencode/internal/tui/styles"
 15	"github.com/opencode-ai/opencode/internal/tui/util"
 16)
 17
 18const id dialogs.DialogID = "sessions"
 19
 20// SessionDialog interface for the session switching dialog
 21type SessionDialog interface {
 22	dialogs.DialogModel
 23}
 24
 25type sessionDialogCmp struct {
 26	selectedInx       int
 27	wWidth            int
 28	wHeight           int
 29	width             int
 30	selectedSessionID string
 31	keyMap            KeyMap
 32	sessionsList      list.ListModel
 33	renderedSelected  bool
 34	help              help.Model
 35}
 36
 37// NewSessionDialogCmp creates a new session switching dialog
 38func NewSessionDialogCmp(sessions []session.Session, selectedID string) SessionDialog {
 39	t := styles.CurrentTheme()
 40	listKeyMap := list.DefaultKeyMap()
 41	keyMap := DefaultKeyMap()
 42
 43	listKeyMap.Down.SetEnabled(false)
 44	listKeyMap.Up.SetEnabled(false)
 45	listKeyMap.NDown.SetEnabled(false)
 46	listKeyMap.NUp.SetEnabled(false)
 47	listKeyMap.HalfPageDown.SetEnabled(false)
 48	listKeyMap.HalfPageUp.SetEnabled(false)
 49	listKeyMap.Home.SetEnabled(false)
 50	listKeyMap.End.SetEnabled(false)
 51
 52	listKeyMap.DownOneItem = keyMap.Next
 53	listKeyMap.UpOneItem = keyMap.Previous
 54
 55	selectedInx := 0
 56	items := make([]util.Model, len(sessions))
 57	if len(sessions) > 0 {
 58		for i, session := range sessions {
 59			items[i] = completions.NewCompletionItem(session.Title, session)
 60			if session.ID == selectedID {
 61				selectedInx = i
 62			}
 63		}
 64	}
 65
 66	sessionsList := list.New(list.WithFilterable(true), list.WithFilterPlaceholder("Enter a session name"), list.WithKeyMap(listKeyMap), list.WithItems(items))
 67	help := help.New()
 68	help.Styles = t.S().Help
 69	s := &sessionDialogCmp{
 70		selectedInx:       selectedInx,
 71		selectedSessionID: selectedID,
 72		keyMap:            DefaultKeyMap(),
 73		sessionsList:      sessionsList,
 74		help:              help,
 75	}
 76
 77	return s
 78}
 79
 80func (s *sessionDialogCmp) Init() tea.Cmd {
 81	return s.sessionsList.Init()
 82}
 83
 84func (s *sessionDialogCmp) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
 85	switch msg := msg.(type) {
 86	case tea.WindowSizeMsg:
 87		s.wWidth = msg.Width
 88		s.wHeight = msg.Height
 89		s.width = s.wWidth / 2
 90		var cmds []tea.Cmd
 91		cmds = append(cmds, s.sessionsList.SetSize(s.listWidth(), s.listHeight()))
 92		if !s.renderedSelected {
 93			cmds = append(cmds, s.sessionsList.SetSelected(s.selectedInx))
 94			s.renderedSelected = true
 95		}
 96		return s, tea.Sequence(cmds...)
 97	case tea.KeyPressMsg:
 98		switch {
 99		case key.Matches(msg, s.keyMap.Select):
100			if len(s.sessionsList.Items()) > 0 {
101				items := s.sessionsList.Items()
102				selectedItemInx := s.sessionsList.SelectedIndex()
103				return s, tea.Sequence(
104					util.CmdHandler(dialogs.CloseDialogMsg{}),
105					util.CmdHandler(
106						chat.SessionSelectedMsg(items[selectedItemInx].(completions.CompletionItem).Value().(session.Session)),
107					),
108				)
109			}
110		default:
111			u, cmd := s.sessionsList.Update(msg)
112			s.sessionsList = u.(list.ListModel)
113			return s, cmd
114		}
115	}
116	return s, nil
117}
118
119func (s *sessionDialogCmp) View() tea.View {
120	t := styles.CurrentTheme()
121	listView := s.sessionsList.View()
122	content := lipgloss.JoinVertical(
123		lipgloss.Left,
124		t.S().Base.Padding(0, 1, 1, 1).Render(core.Title("Switch Session", s.width-4)),
125		listView.String(),
126		"",
127		t.S().Base.Width(s.width-2).PaddingLeft(1).AlignHorizontal(lipgloss.Left).Render(s.help.View(s.keyMap)),
128	)
129
130	v := tea.NewView(s.style().Render(content))
131	if listView.Cursor() != nil {
132		c := s.moveCursor(listView.Cursor())
133		v.SetCursor(c)
134	}
135	return v
136}
137
138func (s *sessionDialogCmp) style() lipgloss.Style {
139	t := styles.CurrentTheme()
140	return t.S().Base.
141		Width(s.width).
142		Border(lipgloss.RoundedBorder()).
143		BorderForeground(t.BorderFocus)
144}
145
146func (s *sessionDialogCmp) listHeight() int {
147	return s.wHeight/2 - 6 // 5 for the border, title and help
148}
149
150func (s *sessionDialogCmp) listWidth() int {
151	return s.width - 2 // 2 for the border
152}
153
154func (s *sessionDialogCmp) Position() (int, int) {
155	row := s.wHeight/4 - 2 // just a bit above the center
156	col := s.wWidth / 2
157	col -= s.width / 2
158	return row, col
159}
160
161func (s *sessionDialogCmp) moveCursor(cursor *tea.Cursor) *tea.Cursor {
162	row, col := s.Position()
163	offset := row + 3 // Border + title
164	cursor.Y += offset
165	cursor.X = cursor.X + col + 2
166	return cursor
167}
168
169// ID implements SessionDialog.
170func (s *sessionDialogCmp) ID() dialogs.DialogID {
171	return id
172}