@@ -7,14 +7,25 @@ import (
7 "charm.land/bubbles/v2/key"
7 "charm.land/bubbles/v2/key"
8 "charm.land/bubbles/v2/textinput"
8 "charm.land/bubbles/v2/textinput"
9 tea "charm.land/bubbletea/v2"
9 tea "charm.land/bubbletea/v2"
10+ "github.com/charmbracelet/crush/internal/session"
10 "github.com/charmbracelet/crush/internal/ui/common"
11 "github.com/charmbracelet/crush/internal/ui/common"
11 "github.com/charmbracelet/crush/internal/ui/list"
12 "github.com/charmbracelet/crush/internal/ui/list"
13+ "github.com/charmbracelet/crush/internal/uiutil"
12 uv "github.com/charmbracelet/ultraviolet"
14 uv "github.com/charmbracelet/ultraviolet"
15// SessionsID is the identifier for the session selector dialog.
17// SessionsID is the identifier for the session selector dialog.
16const SessionsID = "session"
18const SessionsID = "session"
20+type sessionsMode uint8
22+// Possible modes a session item can be in
24+ sessionsModeNormal sessionsMode = iota
18// Session is a session selector dialog.
29// Session is a session selector dialog.
19type Session struct {
30type Session struct {
20 com *common.Common
31 com *common.Common
@@ -22,13 +33,19 @@ type Session struct {
22 list *list.FilterableList
33 list *list.FilterableList
23 input textinput.Model
34 input textinput.Model
24 selectedSessionInx int
35 selectedSessionInx int
36+ sessions []session.Session
38+ sessionsMode sessionsMode
26 keyMap struct {
40 keyMap struct {
27− Select key.Binding
41+ Select key.Binding
28− Next key.Binding
42+ Next key.Binding
29− Previous key.Binding
43+ Previous key.Binding
46+ ConfirmDelete key.Binding
30− UpDown key.Binding
47+ CancelDelete key.Binding
31− Close key.Binding
48+ Close key.Binding
@@ -37,12 +54,14 @@ var _ Dialog = (*Session)(nil)
37// NewSessions creates a new Session dialog.
54// NewSessions creates a new Session dialog.
38func NewSessions(com *common.Common, selectedSessionID string) (*Session, error) {
55func NewSessions(com *common.Common, selectedSessionID string) (*Session, error) {
39 s := new(Session)
56 s := new(Session)
57+ s.sessionsMode = sessionsModeNormal
40 s.com = com
58 s.com = com
41 sessions, err := com.App.Sessions.List(context.TODO())
59 sessions, err := com.App.Sessions.List(context.TODO())
42 if err != nil {
60 if err != nil {
43 return nil, err
61 return nil, err
64+ s.sessions = sessions
46 for i, sess := range sessions {
65 for i, sess := range sessions {
47 if sess.ID == selectedSessionID {
66 if sess.ID == selectedSessionID {
48 s.selectedSessionInx = i
67 s.selectedSessionInx = i
@@ -54,7 +73,7 @@ func NewSessions(com *common.Common, selectedSessionID string) (*Session, error)
54 help.Styles = com.Styles.DialogHelpStyles()
73 help.Styles = com.Styles.DialogHelpStyles()
56 s.help = help
75 s.help = help
57− s.list = list.NewFilterableList(sessionItems(com.Styles, sessions...)...)
76+ s.list = list.NewFilterableList(sessionItems(com.Styles, sessionsModeNormal, sessions...)...)
58 s.list.Focus()
77 s.list.Focus()
59 s.list.SetSelected(s.selectedSessionInx)
78 s.list.SetSelected(s.selectedSessionInx)
@@ -80,6 +99,18 @@ func NewSessions(com *common.Common, selectedSessionID string) (*Session, error)
80 key.WithKeys("up", "down"),
99 key.WithKeys("up", "down"),
81 key.WithHelp("↑↓", "choose"),
100 key.WithHelp("↑↓", "choose"),
102+ s.keyMap.Delete = key.NewBinding(
103+ key.WithKeys("ctrl+x"),
104+ key.WithHelp("ctrl+x", "delete"),
106+ s.keyMap.ConfirmDelete = key.NewBinding(
108+ key.WithHelp("y", "delete"),
110+ s.keyMap.CancelDelete = key.NewBinding(
111+ key.WithKeys("n", "esc"),
112+ key.WithHelp("n", "cancel"),
83 s.keyMap.Close = CloseKey
114 s.keyMap.Close = CloseKey
85 return s, nil
116 return s, nil
@@ -94,40 +125,57 @@ func (s *Session) ID() string {
94func (s *Session) HandleMsg(msg tea.Msg) Action {
125func (s *Session) HandleMsg(msg tea.Msg) Action {
95 switch msg := msg.(type) {
126 switch msg := msg.(type) {
96 case tea.KeyPressMsg:
127 case tea.KeyPressMsg:
97− switch {
128+ switch s.sessionsMode {
129+ case sessionsModeDeleting:
98− case key.Matches(msg, s.keyMap.Close):
131+ case key.Matches(msg, s.keyMap.ConfirmDelete):
99− return ActionClose{}
132+ return s.confirmDeleteSession()
100− case key.Matches(msg, s.keyMap.Previous):
133+ case key.Matches(msg, s.keyMap.CancelDelete):
101− s.list.Focus()
134+ s.sessionsMode = sessionsModeNormal
102− if s.list.IsSelectedFirst() {
135+ s.list.SetItems(sessionItems(s.com.Styles, sessionsModeNormal, s.sessions...)...)
104− s.list.ScrollToBottom()
107− s.list.SelectPrev()
137+ default:
108− s.list.ScrollToSelected()
138+ switch {
139+ case key.Matches(msg, s.keyMap.Close):
140+ return ActionClose{}
141+ case key.Matches(msg, s.keyMap.Delete):
142+ if s.isCurrentSessionBusy() {
143+ return ActionCmd{uiutil.ReportWarn("Agent is busy, please wait...")}
145+ s.sessionsMode = sessionsModeDeleting
146+ s.list.SetItems(sessionItems(s.com.Styles, sessionsModeDeleting, s.sessions...)...)
147+ case key.Matches(msg, s.keyMap.Previous):
149+ if s.list.IsSelectedFirst() {
151+ s.list.ScrollToBottom()
155+ s.list.ScrollToSelected()
109− case key.Matches(msg, s.keyMap.Next):
156+ case key.Matches(msg, s.keyMap.Next):
110− s.list.Focus()
157+ s.list.Focus()
111− if s.list.IsSelectedLast() {
158+ if s.list.IsSelectedLast() {
112− s.list.SelectFirst()
159+ s.list.SelectFirst()
160+ s.list.ScrollToTop()
164+ s.list.ScrollToSelected()
165+ case key.Matches(msg, s.keyMap.Select):
166+ if item := s.list.SelectedItem(); item != nil {
167+ sessionItem := item.(*SessionItem)
168+ return ActionSelectSession{sessionItem.Session}
172+ s.input, cmd = s.input.Update(msg)
173+ value := s.input.Value()
174+ s.list.SetFilter(value)
113 s.list.ScrollToTop()
175 s.list.ScrollToTop()
114− break
176+ s.list.SetSelected(0)
177+ return ActionCmd{cmd}
117− s.list.ScrollToSelected()
118− case key.Matches(msg, s.keyMap.Select):
119− if item := s.list.SelectedItem(); item != nil {
120− sessionItem := item.(*SessionItem)
121− return ActionSelectSession{sessionItem.Session}
125− s.input, cmd = s.input.Update(msg)
126− value := s.input.Value()
127− s.list.SetFilter(value)
128− s.list.ScrollToTop()
129− s.list.SetSelected(0)
130− return ActionCmd{cmd}
133 return nil
181 return nil
@@ -160,27 +208,101 @@ func (s *Session) Draw(scr uv.Screen, area uv.Rectangle) *tea.Cursor {
160 s.list.ScrollToSelected()
208 s.list.ScrollToSelected()
163 rc := NewRenderContext(t, width)
212 rc := NewRenderContext(t, width)
164− rc.Title = "Switch Session"
213+ rc.Title = "Sessions"
214+ switch s.sessionsMode {
215+ case sessionsModeDeleting:
216+ rc.TitleStyle = t.Dialog.Sessions.DeletingTitle
217+ rc.TitleGradientFromColor = t.Dialog.Sessions.DeletingTitleGradientFromColor
218+ rc.TitleGradientToColor = t.Dialog.Sessions.DeletingTitleGradientToColor
219+ rc.ViewStyle = t.Dialog.Sessions.DeletingView
220+ rc.AddPart(t.Dialog.Sessions.DeletingMessage.Render("Delete this session?"))
165− inputView := t.Dialog.InputPrompt.Render(s.input.View())
222+ inputView := t.Dialog.InputPrompt.Render(s.input.View())
166− rc.AddPart(inputView)
224+ rc.AddPart(inputView)
167 listView := t.Dialog.List.Height(s.list.Height()).Render(s.list.Render())
226 listView := t.Dialog.List.Height(s.list.Height()).Render(s.list.Render())
168 rc.AddPart(listView)
227 rc.AddPart(listView)
169 rc.Help = s.help.View(s)
228 rc.Help = s.help.View(s)
171 view := rc.Render()
230 view := rc.Render()
174 DrawCenterCursor(scr, area, view, cur)
232 DrawCenterCursor(scr, area, view, cur)
175 return cur
233 return cur
236+func (s *Session) selectedSessionItem() *SessionItem {
237+ if item := s.list.SelectedItem(); item != nil {
238+ return item.(*SessionItem)
243+func (s *Session) confirmDeleteSession() Action {
244+ sessionItem := s.selectedSessionItem()
245+ s.sessionsMode = sessionsModeNormal
246+ if sessionItem == nil {
250+ s.removeSession(sessionItem.ID())
251+ return ActionCmd{s.deleteSessionCmd(sessionItem.ID())}
254+func (s *Session) removeSession(id string) {
255+ var newSessions []session.Session
256+ for _, sess := range s.sessions {
260+ newSessions = append(newSessions, sess)
262+ s.sessions = newSessions
263+ s.list.SetItems(sessionItems(s.com.Styles, sessionsModeNormal, s.sessions...)...)
264+ s.list.SelectFirst()
265+ s.list.ScrollToSelected()
268+func (s *Session) deleteSessionCmd(id string) tea.Cmd {
269+ return func() tea.Msg {
270+ err := s.com.App.Sessions.Delete(context.TODO(), id)
272+ return uiutil.NewErrorMsg(err)
278+func (s *Session) isCurrentSessionBusy() bool {
279+ sessionItem := s.selectedSessionItem()
280+ if sessionItem == nil {
284+ if s.com.App.AgentCoordinator == nil {
288+ return s.com.App.AgentCoordinator.IsSessionBusy(sessionItem.ID())
178// ShortHelp implements [help.KeyMap].
291// ShortHelp implements [help.KeyMap].
179func (s *Session) ShortHelp() []key.Binding {
292func (s *Session) ShortHelp() []key.Binding {
293+ switch s.sessionsMode {
294+ case sessionsModeDeleting:
180− return []key.Binding{
295+ return []key.Binding{
296+ s.keyMap.ConfirmDelete,
297+ s.keyMap.CancelDelete,
300+ return []key.Binding{
181− s.keyMap.UpDown,
301+ s.keyMap.UpDown,
182− s.keyMap.Select,
302+ s.keyMap.Select,
183− s.keyMap.Close,
304+ s.keyMap.Close,
@@ -191,8 +313,17 @@ func (s *Session) FullHelp() [][]key.Binding {
191 s.keyMap.Select,
313 s.keyMap.Select,
192 s.keyMap.Next,
314 s.keyMap.Next,
193 s.keyMap.Previous,
315 s.keyMap.Previous,
194 s.keyMap.Close,
317 s.keyMap.Close,
320+ switch s.sessionsMode {
321+ case sessionsModeDeleting:
322+ slice = []key.Binding{
323+ s.keyMap.ConfirmDelete,
324+ s.keyMap.CancelDelete,
196 for i := 0; i < len(slice); i += 4 {
327 for i := 0; i < len(slice); i += 4 {
197 end := min(i+4, len(slice))
328 end := min(i+4, len(slice))
198 m = append(m, slice[i:end])
329 m = append(m, slice[i:end])