@@ -0,0 +1,172 @@
1+package sessions
2+
3+import (
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+
18+const id dialogs.DialogID = "sessions"
19+
20+// SessionDialog interface for the session switching dialog
21+type SessionDialog interface {
22+ dialogs.DialogModel
23+}
24+
25+type 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
38+func 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+
80+func (s *sessionDialogCmp) Init() tea.Cmd {
81+ return s.sessionsList.Init()
82+}
83+
84+func (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+
119+func (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).PaddingRight(2).AlignHorizontal(lipgloss.Right).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+
138+func (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+
146+func (s *sessionDialogCmp) listHeight() int {
147+ return s.wHeight/2 - 6 // 5 for the border, title and help
148+}
149+
150+func (s *sessionDialogCmp) listWidth() int {
151+ return s.width - 2 // 2 for the border
152+}
153+
154+func (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+
161+func (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.
170+func (s *sessionDialogCmp) ID() dialogs.DialogID {
171+ return id
172+}