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}