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(
 67		list.WithFilterable(true),
 68		list.WithFilterPlaceholder("Enter a session name"),
 69		list.WithKeyMap(listKeyMap),
 70		list.WithItems(items),
 71		list.WithWrapNavigation(true),
 72	)
 73	help := help.New()
 74	help.Styles = t.S().Help
 75	s := &sessionDialogCmp{
 76		selectedInx:       selectedInx,
 77		selectedSessionID: selectedID,
 78		keyMap:            DefaultKeyMap(),
 79		sessionsList:      sessionsList,
 80		help:              help,
 81	}
 82
 83	return s
 84}
 85
 86func (s *sessionDialogCmp) Init() tea.Cmd {
 87	return s.sessionsList.Init()
 88}
 89
 90func (s *sessionDialogCmp) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
 91	switch msg := msg.(type) {
 92	case tea.WindowSizeMsg:
 93		s.wWidth = msg.Width
 94		s.wHeight = msg.Height
 95		s.width = s.wWidth / 2
 96		var cmds []tea.Cmd
 97		cmds = append(cmds, s.sessionsList.SetSize(s.listWidth(), s.listHeight()))
 98		if !s.renderedSelected {
 99			cmds = append(cmds, s.sessionsList.SetSelected(s.selectedInx))
100			s.renderedSelected = true
101		}
102		return s, tea.Sequence(cmds...)
103	case tea.KeyPressMsg:
104		switch {
105		case key.Matches(msg, s.keyMap.Select):
106			if len(s.sessionsList.Items()) > 0 {
107				items := s.sessionsList.Items()
108				selectedItemInx := s.sessionsList.SelectedIndex()
109				return s, tea.Sequence(
110					util.CmdHandler(dialogs.CloseDialogMsg{}),
111					util.CmdHandler(
112						chat.SessionSelectedMsg(items[selectedItemInx].(completions.CompletionItem).Value().(session.Session)),
113					),
114				)
115			}
116		default:
117			u, cmd := s.sessionsList.Update(msg)
118			s.sessionsList = u.(list.ListModel)
119			return s, cmd
120		}
121	}
122	return s, nil
123}
124
125func (s *sessionDialogCmp) View() tea.View {
126	t := styles.CurrentTheme()
127	listView := s.sessionsList.View()
128	content := lipgloss.JoinVertical(
129		lipgloss.Left,
130		t.S().Base.Padding(0, 1, 1, 1).Render(core.Title("Switch Session", s.width-4)),
131		listView.String(),
132		"",
133		t.S().Base.Width(s.width-2).PaddingLeft(1).AlignHorizontal(lipgloss.Left).Render(s.help.View(s.keyMap)),
134	)
135
136	v := tea.NewView(s.style().Render(content))
137	if listView.Cursor() != nil {
138		c := s.moveCursor(listView.Cursor())
139		v.SetCursor(c)
140	}
141	return v
142}
143
144func (s *sessionDialogCmp) style() lipgloss.Style {
145	t := styles.CurrentTheme()
146	return t.S().Base.
147		Width(s.width).
148		Border(lipgloss.RoundedBorder()).
149		BorderForeground(t.BorderFocus)
150}
151
152func (s *sessionDialogCmp) listHeight() int {
153	return s.wHeight/2 - 6 // 5 for the border, title and help
154}
155
156func (s *sessionDialogCmp) listWidth() int {
157	return s.width - 2 // 2 for the border
158}
159
160func (s *sessionDialogCmp) Position() (int, int) {
161	row := s.wHeight/4 - 2 // just a bit above the center
162	col := s.wWidth / 2
163	col -= s.width / 2
164	return row, col
165}
166
167func (s *sessionDialogCmp) moveCursor(cursor *tea.Cursor) *tea.Cursor {
168	row, col := s.Position()
169	offset := row + 3 // Border + title
170	cursor.Y += offset
171	cursor.X = cursor.X + col + 2
172	return cursor
173}
174
175// ID implements SessionDialog.
176func (s *sessionDialogCmp) ID() dialogs.DialogID {
177	return id
178}