diff options
| author | Adam <[email protected]> | 2025-06-24 11:07:41 -0500 |
|---|---|---|
| committer | GitHub <[email protected]> | 2025-06-24 11:07:41 -0500 |
| commit | 6f1847542891421e2be44218c7e31fb329582452 (patch) | |
| tree | 96c65ea0c708954fecc8bcad18c2e8757a6255f4 /packages/tui/internal/components | |
| parent | 3664b09812352795fc9855b9a921fdd2ca293a14 (diff) | |
| download | opencode-6f1847542891421e2be44218c7e31fb329582452.tar.gz opencode-6f1847542891421e2be44218c7e31fb329582452.zip | |
feat: delete sessions (#362)
Co-authored-by: adamdottv <[email protected]>
Diffstat (limited to 'packages/tui/internal/components')
| -rw-r--r-- | packages/tui/internal/components/dialog/session.go | 162 |
1 files changed, 148 insertions, 14 deletions
diff --git a/packages/tui/internal/components/dialog/session.go b/packages/tui/internal/components/dialog/session.go index 2887ae736..71f3f2e9e 100644 --- a/packages/tui/internal/components/dialog/session.go +++ b/packages/tui/internal/components/dialog/session.go @@ -2,12 +2,20 @@ package dialog import ( "context" + "strings" + + "slices" tea "github.com/charmbracelet/bubbletea/v2" + "github.com/charmbracelet/lipgloss/v2" + "github.com/muesli/reflow/truncate" "github.com/sst/opencode/internal/app" "github.com/sst/opencode/internal/components/list" "github.com/sst/opencode/internal/components/modal" + "github.com/sst/opencode/internal/components/toast" "github.com/sst/opencode/internal/layout" + "github.com/sst/opencode/internal/styles" + "github.com/sst/opencode/internal/theme" "github.com/sst/opencode/internal/util" "github.com/sst/opencode/pkg/client" ) @@ -17,12 +25,65 @@ type SessionDialog interface { layout.Modal } +// sessionItem is a custom list item for sessions that can show delete confirmation +type sessionItem struct { + title string + isDeleteConfirming bool +} + +func (s sessionItem) Render(selected bool, width int) string { + t := theme.CurrentTheme() + baseStyle := styles.BaseStyle() + + var text string + if s.isDeleteConfirming { + text = "Press again to confirm delete" + } else { + text = s.title + } + + truncatedStr := truncate.StringWithTail(text, uint(width-1), "...") + + var itemStyle lipgloss.Style + if selected { + if s.isDeleteConfirming { + // Red background for delete confirmation + itemStyle = baseStyle. + Background(t.Error()). + Foreground(t.Background()). + Width(width). + PaddingLeft(1) + } else { + // Normal selection + itemStyle = baseStyle. + Background(t.Primary()). + Foreground(t.Background()). + Width(width). + PaddingLeft(1) + } + } else { + if s.isDeleteConfirming { + // Red text for delete confirmation when not selected + itemStyle = baseStyle. + Foreground(t.Error()). + PaddingLeft(1) + } else { + itemStyle = baseStyle. + PaddingLeft(1) + } + } + + return itemStyle.Render(truncatedStr) +} + type sessionDialog struct { - width int - height int - modal *modal.Modal - sessions []client.SessionInfo - list list.List[list.StringItem] + width int + height int + modal *modal.Modal + sessions []client.SessionInfo + list list.List[sessionItem] + app *app.App + deleteConfirmation int // -1 means no confirmation, >= 0 means confirming deletion of session at this index } func (s *sessionDialog) Init() tea.Cmd { @@ -38,6 +99,11 @@ func (s *sessionDialog) Update(msg tea.Msg) (tea.Model, tea.Cmd) { case tea.KeyPressMsg: switch msg.String() { case "enter": + if s.deleteConfirmation >= 0 { + s.deleteConfirmation = -1 + s.updateListItems() + return s, nil + } if _, idx := s.list.GetSelectedItem(); idx >= 0 && idx < len(s.sessions) { selectedSession := s.sessions[idx] return s, tea.Sequence( @@ -45,17 +111,79 @@ func (s *sessionDialog) Update(msg tea.Msg) (tea.Model, tea.Cmd) { util.CmdHandler(app.SessionSelectedMsg(&selectedSession)), ) } + case "x", "delete", "backspace": + if _, idx := s.list.GetSelectedItem(); idx >= 0 && idx < len(s.sessions) { + if s.deleteConfirmation == idx { + // Second press - actually delete the session + sessionToDelete := s.sessions[idx] + return s, tea.Sequence( + func() tea.Msg { + s.sessions = slices.Delete(s.sessions, idx, idx+1) + s.deleteConfirmation = -1 + s.updateListItems() + return nil + }, + s.deleteSession(sessionToDelete.Id), + ) + } else { + // First press - enter delete confirmation mode + s.deleteConfirmation = idx + s.updateListItems() + return s, nil + } + } + case "esc": + if s.deleteConfirmation >= 0 { + s.deleteConfirmation = -1 + s.updateListItems() + return s, nil + } } } var cmd tea.Cmd listModel, cmd := s.list.Update(msg) - s.list = listModel.(list.List[list.StringItem]) + s.list = listModel.(list.List[sessionItem]) return s, cmd } func (s *sessionDialog) Render(background string) string { - return s.modal.Render(s.list.View(), background) + listView := s.list.View() + + t := theme.CurrentTheme() + helpStyle := styles.BaseStyle().PaddingLeft(1).PaddingTop(1) + helpText := styles.BaseStyle().Foreground(t.Text()).Render("x/del") + helpText = helpText + styles.BaseStyle().Background(t.BackgroundElement()).Foreground(t.TextMuted()).Render(" delete session") + helpText = helpStyle.Render(helpText) + + content := strings.Join([]string{listView, helpText}, "\n") + + return s.modal.Render(content, background) +} + +func (s *sessionDialog) updateListItems() { + _, currentIdx := s.list.GetSelectedItem() + + var items []sessionItem + for i, sess := range s.sessions { + item := sessionItem{ + title: sess.Title, + isDeleteConfirming: s.deleteConfirmation == i, + } + items = append(items, item) + } + s.list.SetItems(items) + s.list.SetSelectedIndex(currentIdx) +} + +func (s *sessionDialog) deleteSession(sessionID string) tea.Cmd { + return func() tea.Msg { + ctx := context.Background() + if err := s.app.DeleteSession(ctx, sessionID); err != nil { + return toast.NewErrorToast("Failed to delete session: " + err.Error())() + } + return nil + } } func (s *sessionDialog) Close() tea.Cmd { @@ -67,26 +195,32 @@ func NewSessionDialog(app *app.App) SessionDialog { sessions, _ := app.ListSessions(context.Background()) var filteredSessions []client.SessionInfo - var sessionTitles []string + var items []sessionItem for _, sess := range sessions { if sess.ParentID != nil { continue } filteredSessions = append(filteredSessions, sess) - sessionTitles = append(sessionTitles, sess.Title) + items = append(items, sessionItem{ + title: sess.Title, + isDeleteConfirming: false, + }) } - list := list.NewStringList( - sessionTitles, + // Create a generic list component + listComponent := list.NewListComponent( + items, 10, // maxVisibleSessions "No sessions available", true, // useAlphaNumericKeys ) - list.SetMaxWidth(layout.Current.Container.Width - 12) + listComponent.SetMaxWidth(layout.Current.Container.Width - 12) return &sessionDialog{ - sessions: filteredSessions, - list: list, + sessions: filteredSessions, + list: listComponent, + app: app, + deleteConfirmation: -1, modal: modal.New( modal.WithTitle("Switch Session"), modal.WithMaxWidth(layout.Current.Container.Width-8), |
