@@ -7,14 +7,25 @@ import (
77 "charm.land/bubbles/v2/key"
88 "charm.land/bubbles/v2/textinput"
99 tea "charm.land/bubbletea/v2"
10+ "github.com/charmbracelet/crush/internal/session"
1011 "github.com/charmbracelet/crush/internal/ui/common"
1112 "github.com/charmbracelet/crush/internal/ui/list"
13+ "github.com/charmbracelet/crush/internal/uiutil"
1214 uv "github.com/charmbracelet/ultraviolet"
1315)
1416
1517// SessionsID is the identifier for the session selector dialog.
1618const SessionsID = "session"
1719
20+type sessionsMode uint8
21+
22+// Possible modes a session item can be in
23+const (
24+ sessionsModeNormal sessionsMode = iota
25+ sessionsModeDeleting
26+ sessionsModeUpdating
27+)
28+
1829// Session is a session selector dialog.
1930type Session struct {
2031 com *common.Common
@@ -22,13 +33,19 @@ type Session struct {
2233 list *list.FilterableList
2334 input textinput.Model
2435 selectedSessionInx int
36+ sessions []session.Session
37+
38+ sessionsMode sessionsMode
2539
2640 keyMap struct {
27− Select key.Binding
28− Next key.Binding
29− Previous key.Binding
30− UpDown key.Binding
31− Close key.Binding
41+ Select key.Binding
42+ Next key.Binding
43+ Previous key.Binding
44+ UpDown key.Binding
45+ Delete key.Binding
46+ ConfirmDelete key.Binding
47+ CancelDelete key.Binding
48+ Close key.Binding
3249 }
3350}
3451
@@ -37,12 +54,14 @@ var _ Dialog = (*Session)(nil)
3754// NewSessions creates a new Session dialog.
3855func NewSessions(com *common.Common, selectedSessionID string) (*Session, error) {
3956 s := new(Session)
57+ s.sessionsMode = sessionsModeNormal
4058 s.com = com
4159 sessions, err := com.App.Sessions.List(context.TODO())
4260 if err != nil {
4361 return nil, err
4462 }
4563
64+ s.sessions = sessions
4665 for i, sess := range sessions {
4766 if sess.ID == selectedSessionID {
4867 s.selectedSessionInx = i
@@ -54,7 +73,7 @@ func NewSessions(com *common.Common, selectedSessionID string) (*Session, error)
5473 help.Styles = com.Styles.DialogHelpStyles()
5574
5675 s.help = help
57− s.list = list.NewFilterableList(sessionItems(com.Styles, sessions...)...)
76+ s.list = list.NewFilterableList(sessionItems(com.Styles, sessionsModeNormal, sessions...)...)
5877 s.list.Focus()
5978 s.list.SetSelected(s.selectedSessionInx)
6079
@@ -80,6 +99,18 @@ func NewSessions(com *common.Common, selectedSessionID string) (*Session, error)
8099 key.WithKeys("up", "down"),
81100 key.WithHelp("↑↓", "choose"),
82101 )
102+ s.keyMap.Delete = key.NewBinding(
103+ key.WithKeys("ctrl+x"),
104+ key.WithHelp("ctrl+x", "delete"),
105+ )
106+ s.keyMap.ConfirmDelete = key.NewBinding(
107+ key.WithKeys("y"),
108+ key.WithHelp("y", "delete"),
109+ )
110+ s.keyMap.CancelDelete = key.NewBinding(
111+ key.WithKeys("n", "esc"),
112+ key.WithHelp("n", "cancel"),
113+ )
83114 s.keyMap.Close = CloseKey
84115
85116 return s, nil
@@ -94,40 +125,57 @@ func (s *Session) ID() string {
94125func (s *Session) HandleMsg(msg tea.Msg) Action {
95126 switch msg := msg.(type) {
96127 case tea.KeyPressMsg:
97− switch {
98− case key.Matches(msg, s.keyMap.Close):
99− return ActionClose{}
100− case key.Matches(msg, s.keyMap.Previous):
101− s.list.Focus()
102− if s.list.IsSelectedFirst() {
103− s.list.SelectLast()
104− s.list.ScrollToBottom()
105− break
128+ switch s.sessionsMode {
129+ case sessionsModeDeleting:
130+ switch {
131+ case key.Matches(msg, s.keyMap.ConfirmDelete):
132+ return s.confirmDeleteSession()
133+ case key.Matches(msg, s.keyMap.CancelDelete):
134+ s.sessionsMode = sessionsModeNormal
135+ s.list.SetItems(sessionItems(s.com.Styles, sessionsModeNormal, s.sessions...)...)
106136 }
107− s.list.SelectPrev()
108− s.list.ScrollToSelected()
109− case key.Matches(msg, s.keyMap.Next):
110− s.list.Focus()
111− if s.list.IsSelectedLast() {
112− s.list.SelectFirst()
137+ default:
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...")}
144+ }
145+ s.sessionsMode = sessionsModeDeleting
146+ s.list.SetItems(sessionItems(s.com.Styles, sessionsModeDeleting, s.sessions...)...)
147+ case key.Matches(msg, s.keyMap.Previous):
148+ s.list.Focus()
149+ if s.list.IsSelectedFirst() {
150+ s.list.SelectLast()
151+ s.list.ScrollToBottom()
152+ break
153+ }
154+ s.list.SelectPrev()
155+ s.list.ScrollToSelected()
156+ case key.Matches(msg, s.keyMap.Next):
157+ s.list.Focus()
158+ if s.list.IsSelectedLast() {
159+ s.list.SelectFirst()
160+ s.list.ScrollToTop()
161+ break
162+ }
163+ s.list.SelectNext()
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}
169+ }
170+ default:
171+ var cmd tea.Cmd
172+ s.input, cmd = s.input.Update(msg)
173+ value := s.input.Value()
174+ s.list.SetFilter(value)
113175 s.list.ScrollToTop()
114− break
176+ s.list.SetSelected(0)
177+ return ActionCmd{cmd}
115178 }
116− s.list.SelectNext()
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}
122− }
123− default:
124− var cmd tea.Cmd
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}
131179 }
132180 }
133181 return nil
@@ -160,27 +208,101 @@ func (s *Session) Draw(scr uv.Screen, area uv.Rectangle) *tea.Cursor {
160208 s.list.ScrollToSelected()
161209 }
162210
211+ var cur *tea.Cursor
163212 rc := NewRenderContext(t, width)
164− rc.Title = "Switch Session"
165− inputView := t.Dialog.InputPrompt.Render(s.input.View())
166− rc.AddPart(inputView)
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?"))
221+ default:
222+ inputView := t.Dialog.InputPrompt.Render(s.input.View())
223+ cur = s.Cursor()
224+ rc.AddPart(inputView)
225+ }
167226 listView := t.Dialog.List.Height(s.list.Height()).Render(s.list.Render())
168227 rc.AddPart(listView)
169228 rc.Help = s.help.View(s)
170229
171230 view := rc.Render()
172231
173− cur := s.Cursor()
174232 DrawCenterCursor(scr, area, view, cur)
175233 return cur
176234}
177235
236+func (s *Session) selectedSessionItem() *SessionItem {
237+ if item := s.list.SelectedItem(); item != nil {
238+ return item.(*SessionItem)
239+ }
240+ return nil
241+}
242+
243+func (s *Session) confirmDeleteSession() Action {
244+ sessionItem := s.selectedSessionItem()
245+ s.sessionsMode = sessionsModeNormal
246+ if sessionItem == nil {
247+ return nil
248+ }
249+
250+ s.removeSession(sessionItem.ID())
251+ return ActionCmd{s.deleteSessionCmd(sessionItem.ID())}
252+}
253+
254+func (s *Session) removeSession(id string) {
255+ var newSessions []session.Session
256+ for _, sess := range s.sessions {
257+ if sess.ID == id {
258+ continue
259+ }
260+ newSessions = append(newSessions, sess)
261+ }
262+ s.sessions = newSessions
263+ s.list.SetItems(sessionItems(s.com.Styles, sessionsModeNormal, s.sessions...)...)
264+ s.list.SelectFirst()
265+ s.list.ScrollToSelected()
266+}
267+
268+func (s *Session) deleteSessionCmd(id string) tea.Cmd {
269+ return func() tea.Msg {
270+ err := s.com.App.Sessions.Delete(context.TODO(), id)
271+ if err != nil {
272+ return uiutil.NewErrorMsg(err)
273+ }
274+ return nil
275+ }
276+}
277+
278+func (s *Session) isCurrentSessionBusy() bool {
279+ sessionItem := s.selectedSessionItem()
280+ if sessionItem == nil {
281+ return false
282+ }
283+
284+ if s.com.App.AgentCoordinator == nil {
285+ return false
286+ }
287+
288+ return s.com.App.AgentCoordinator.IsSessionBusy(sessionItem.ID())
289+}
290+
178291// ShortHelp implements [help.KeyMap].
179292func (s *Session) ShortHelp() []key.Binding {
180− return []key.Binding{
181− s.keyMap.UpDown,
182− s.keyMap.Select,
183− s.keyMap.Close,
293+ switch s.sessionsMode {
294+ case sessionsModeDeleting:
295+ return []key.Binding{
296+ s.keyMap.ConfirmDelete,
297+ s.keyMap.CancelDelete,
298+ }
299+ default:
300+ return []key.Binding{
301+ s.keyMap.UpDown,
302+ s.keyMap.Select,
303+ s.keyMap.Delete,
304+ s.keyMap.Close,
305+ }
184306 }
185307}
186308
@@ -191,8 +313,17 @@ func (s *Session) FullHelp() [][]key.Binding {
191313 s.keyMap.Select,
192314 s.keyMap.Next,
193315 s.keyMap.Previous,
316+ s.keyMap.Delete,
194317 s.keyMap.Close,
195318 }
319+
320+ switch s.sessionsMode {
321+ case sessionsModeDeleting:
322+ slice = []key.Binding{
323+ s.keyMap.ConfirmDelete,
324+ s.keyMap.CancelDelete,
325+ }
326+ }
196327 for i := 0; i < len(slice); i += 4 {
197328 end := min(i+4, len(slice))
198329 m = append(m, slice[i:end])