summaryrefslogtreecommitdiffhomepage
path: root/packages/tui/internal/components
diff options
context:
space:
mode:
authorAdam <[email protected]>2025-06-24 11:07:41 -0500
committerGitHub <[email protected]>2025-06-24 11:07:41 -0500
commit6f1847542891421e2be44218c7e31fb329582452 (patch)
tree96c65ea0c708954fecc8bcad18c2e8757a6255f4 /packages/tui/internal/components
parent3664b09812352795fc9855b9a921fdd2ca293a14 (diff)
downloadopencode-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.go162
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),