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}