|
@@ -6,6 +6,7 @@ import (
|
|
|
|
|
|
|
|
"slices"
|
|
"slices"
|
|
|
|
|
|
|
|
|
|
+ "github.com/charmbracelet/bubbles/v2/textinput"
|
|
|
tea "github.com/charmbracelet/bubbletea/v2"
|
|
tea "github.com/charmbracelet/bubbletea/v2"
|
|
|
"github.com/muesli/reflow/truncate"
|
|
"github.com/muesli/reflow/truncate"
|
|
|
"github.com/sst/opencode-sdk-go"
|
|
"github.com/sst/opencode-sdk-go"
|
|
@@ -110,6 +111,9 @@ type sessionDialog struct {
|
|
|
list list.List[sessionItem]
|
|
list list.List[sessionItem]
|
|
|
app *app.App
|
|
app *app.App
|
|
|
deleteConfirmation int // -1 means no confirmation, >= 0 means confirming deletion of session at this index
|
|
deleteConfirmation int // -1 means no confirmation, >= 0 means confirming deletion of session at this index
|
|
|
|
|
+ renameMode bool
|
|
|
|
|
+ renameInput textinput.Model
|
|
|
|
|
+ renameIndex int // index of session being renamed
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
func (s *sessionDialog) Init() tea.Cmd {
|
|
func (s *sessionDialog) Init() tea.Cmd {
|
|
@@ -123,69 +127,128 @@ func (s *sessionDialog) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
|
|
s.height = msg.Height
|
|
s.height = msg.Height
|
|
|
s.list.SetMaxWidth(layout.Current.Container.Width - 12)
|
|
s.list.SetMaxWidth(layout.Current.Container.Width - 12)
|
|
|
case tea.KeyPressMsg:
|
|
case tea.KeyPressMsg:
|
|
|
- switch msg.String() {
|
|
|
|
|
- case "enter":
|
|
|
|
|
- if s.deleteConfirmation >= 0 {
|
|
|
|
|
- s.deleteConfirmation = -1
|
|
|
|
|
|
|
+ if s.renameMode {
|
|
|
|
|
+ switch msg.String() {
|
|
|
|
|
+ case "enter":
|
|
|
|
|
+ if _, idx := s.list.GetSelectedItem(); idx >= 0 && idx < len(s.sessions) && idx == s.renameIndex {
|
|
|
|
|
+ newTitle := s.renameInput.Value()
|
|
|
|
|
+ if strings.TrimSpace(newTitle) != "" {
|
|
|
|
|
+ sessionToUpdate := s.sessions[idx]
|
|
|
|
|
+ return s, tea.Sequence(
|
|
|
|
|
+ func() tea.Msg {
|
|
|
|
|
+ ctx := context.Background()
|
|
|
|
|
+ err := s.app.UpdateSession(ctx, sessionToUpdate.ID, newTitle)
|
|
|
|
|
+ if err != nil {
|
|
|
|
|
+ return toast.NewErrorToast("Failed to rename session: " + err.Error())()
|
|
|
|
|
+ }
|
|
|
|
|
+ s.sessions[idx].Title = newTitle
|
|
|
|
|
+ s.renameMode = false
|
|
|
|
|
+ s.modal.SetTitle("Switch Session")
|
|
|
|
|
+ s.updateListItems()
|
|
|
|
|
+ return toast.NewSuccessToast("Session renamed successfully")()
|
|
|
|
|
+ },
|
|
|
|
|
+ )
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ s.renameMode = false
|
|
|
|
|
+ s.modal.SetTitle("Switch Session")
|
|
|
s.updateListItems()
|
|
s.updateListItems()
|
|
|
return s, nil
|
|
return s, nil
|
|
|
|
|
+ default:
|
|
|
|
|
+ var cmd tea.Cmd
|
|
|
|
|
+ s.renameInput, cmd = s.renameInput.Update(msg)
|
|
|
|
|
+ return s, cmd
|
|
|
}
|
|
}
|
|
|
- if _, idx := s.list.GetSelectedItem(); idx >= 0 && idx < len(s.sessions) {
|
|
|
|
|
- selectedSession := s.sessions[idx]
|
|
|
|
|
|
|
+ } else {
|
|
|
|
|
+ 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(
|
|
|
|
|
+ util.CmdHandler(modal.CloseModalMsg{}),
|
|
|
|
|
+ util.CmdHandler(app.SessionSelectedMsg(&selectedSession)),
|
|
|
|
|
+ )
|
|
|
|
|
+ }
|
|
|
|
|
+ case "n":
|
|
|
return s, tea.Sequence(
|
|
return s, tea.Sequence(
|
|
|
util.CmdHandler(modal.CloseModalMsg{}),
|
|
util.CmdHandler(modal.CloseModalMsg{}),
|
|
|
- util.CmdHandler(app.SessionSelectedMsg(&selectedSession)),
|
|
|
|
|
|
|
+ util.CmdHandler(app.SessionClearedMsg{}),
|
|
|
)
|
|
)
|
|
|
- }
|
|
|
|
|
- case "n":
|
|
|
|
|
- return s, tea.Sequence(
|
|
|
|
|
- util.CmdHandler(modal.CloseModalMsg{}),
|
|
|
|
|
- util.CmdHandler(app.SessionClearedMsg{}),
|
|
|
|
|
- )
|
|
|
|
|
- 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
|
|
|
|
|
|
|
+ case "r":
|
|
|
|
|
+ if _, idx := s.list.GetSelectedItem(); idx >= 0 && idx < len(s.sessions) {
|
|
|
|
|
+ s.renameMode = true
|
|
|
|
|
+ s.renameIndex = idx
|
|
|
|
|
+ s.setupRenameInput(s.sessions[idx].Title)
|
|
|
|
|
+ s.modal.SetTitle("Rename Session")
|
|
|
|
|
+ s.updateListItems()
|
|
|
|
|
+ return s, textinput.Blink
|
|
|
|
|
+ }
|
|
|
|
|
+ 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()
|
|
s.updateListItems()
|
|
|
return s, nil
|
|
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[sessionItem])
|
|
|
|
|
- return s, cmd
|
|
|
|
|
|
|
+ if !s.renameMode {
|
|
|
|
|
+ var cmd tea.Cmd
|
|
|
|
|
+ listModel, cmd := s.list.Update(msg)
|
|
|
|
|
+ s.list = listModel.(list.List[sessionItem])
|
|
|
|
|
+ return s, cmd
|
|
|
|
|
+ }
|
|
|
|
|
+ return s, nil
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
func (s *sessionDialog) Render(background string) string {
|
|
func (s *sessionDialog) Render(background string) string {
|
|
|
|
|
+ if s.renameMode {
|
|
|
|
|
+ // Show rename input instead of list
|
|
|
|
|
+ t := theme.CurrentTheme()
|
|
|
|
|
+ renameView := s.renameInput.View()
|
|
|
|
|
+
|
|
|
|
|
+ mutedStyle := styles.NewStyle().Foreground(t.TextMuted()).Background(t.BackgroundPanel()).Render
|
|
|
|
|
+ helpText := mutedStyle("Enter to confirm, Esc to cancel")
|
|
|
|
|
+ helpText = styles.NewStyle().PaddingLeft(1).PaddingTop(1).Render(helpText)
|
|
|
|
|
+
|
|
|
|
|
+ content := strings.Join([]string{renameView, helpText}, "\n")
|
|
|
|
|
+ return s.modal.Render(content, background)
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
listView := s.list.View()
|
|
listView := s.list.View()
|
|
|
|
|
|
|
|
t := theme.CurrentTheme()
|
|
t := theme.CurrentTheme()
|
|
|
keyStyle := styles.NewStyle().Foreground(t.Text()).Background(t.BackgroundPanel()).Render
|
|
keyStyle := styles.NewStyle().Foreground(t.Text()).Background(t.BackgroundPanel()).Render
|
|
|
mutedStyle := styles.NewStyle().Foreground(t.TextMuted()).Background(t.BackgroundPanel()).Render
|
|
mutedStyle := styles.NewStyle().Foreground(t.TextMuted()).Background(t.BackgroundPanel()).Render
|
|
|
|
|
|
|
|
- leftHelp := keyStyle("n") + mutedStyle(" new session")
|
|
|
|
|
|
|
+ leftHelp := keyStyle("n") + mutedStyle(" new session") + " " + keyStyle("r") + mutedStyle(" rename")
|
|
|
rightHelp := keyStyle("x/del") + mutedStyle(" delete session")
|
|
rightHelp := keyStyle("x/del") + mutedStyle(" delete session")
|
|
|
|
|
|
|
|
bgColor := t.BackgroundPanel()
|
|
bgColor := t.BackgroundPanel()
|
|
@@ -203,6 +266,39 @@ func (s *sessionDialog) Render(background string) string {
|
|
|
return s.modal.Render(content, background)
|
|
return s.modal.Render(content, background)
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
+func (s *sessionDialog) setupRenameInput(currentTitle string) {
|
|
|
|
|
+ t := theme.CurrentTheme()
|
|
|
|
|
+ bgColor := t.BackgroundPanel()
|
|
|
|
|
+ textColor := t.Text()
|
|
|
|
|
+ textMutedColor := t.TextMuted()
|
|
|
|
|
+
|
|
|
|
|
+ s.renameInput = textinput.New()
|
|
|
|
|
+ s.renameInput.SetValue(currentTitle)
|
|
|
|
|
+ s.renameInput.Focus()
|
|
|
|
|
+ s.renameInput.CharLimit = 100
|
|
|
|
|
+ s.renameInput.SetWidth(layout.Current.Container.Width - 20)
|
|
|
|
|
+
|
|
|
|
|
+ s.renameInput.Styles.Blurred.Placeholder = styles.NewStyle().
|
|
|
|
|
+ Foreground(textMutedColor).
|
|
|
|
|
+ Background(bgColor).
|
|
|
|
|
+ Lipgloss()
|
|
|
|
|
+ s.renameInput.Styles.Blurred.Text = styles.NewStyle().
|
|
|
|
|
+ Foreground(textColor).
|
|
|
|
|
+ Background(bgColor).
|
|
|
|
|
+ Lipgloss()
|
|
|
|
|
+ s.renameInput.Styles.Focused.Placeholder = styles.NewStyle().
|
|
|
|
|
+ Foreground(textMutedColor).
|
|
|
|
|
+ Background(bgColor).
|
|
|
|
|
+ Lipgloss()
|
|
|
|
|
+ s.renameInput.Styles.Focused.Text = styles.NewStyle().
|
|
|
|
|
+ Foreground(textColor).
|
|
|
|
|
+ Background(bgColor).
|
|
|
|
|
+ Lipgloss()
|
|
|
|
|
+ s.renameInput.Styles.Focused.Prompt = styles.NewStyle().
|
|
|
|
|
+ Background(bgColor).
|
|
|
|
|
+ Lipgloss()
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
func (s *sessionDialog) updateListItems() {
|
|
func (s *sessionDialog) updateListItems() {
|
|
|
_, currentIdx := s.list.GetSelectedItem()
|
|
_, currentIdx := s.list.GetSelectedItem()
|
|
|
|
|
|
|
@@ -229,7 +325,22 @@ func (s *sessionDialog) deleteSession(sessionID string) tea.Cmd {
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
+// ReopenSessionModalMsg is emitted when the session modal should be reopened
|
|
|
|
|
+type ReopenSessionModalMsg struct{}
|
|
|
|
|
+
|
|
|
func (s *sessionDialog) Close() tea.Cmd {
|
|
func (s *sessionDialog) Close() tea.Cmd {
|
|
|
|
|
+ if s.renameMode {
|
|
|
|
|
+ // If in rename mode, exit rename mode and return a command to reopen the modal
|
|
|
|
|
+ s.renameMode = false
|
|
|
|
|
+ s.modal.SetTitle("Switch Session")
|
|
|
|
|
+ s.updateListItems()
|
|
|
|
|
+
|
|
|
|
|
+ // Return a command that will reopen the session modal
|
|
|
|
|
+ return func() tea.Msg {
|
|
|
|
|
+ return ReopenSessionModalMsg{}
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ // Normal close behavior
|
|
|
return nil
|
|
return nil
|
|
|
}
|
|
}
|
|
|
|
|
|
|
@@ -272,6 +383,8 @@ func NewSessionDialog(app *app.App) SessionDialog {
|
|
|
list: listComponent,
|
|
list: listComponent,
|
|
|
app: app,
|
|
app: app,
|
|
|
deleteConfirmation: -1,
|
|
deleteConfirmation: -1,
|
|
|
|
|
+ renameMode: false,
|
|
|
|
|
+ renameIndex: -1,
|
|
|
modal: modal.New(
|
|
modal: modal.New(
|
|
|
modal.WithTitle("Switch Session"),
|
|
modal.WithTitle("Switch Session"),
|
|
|
modal.WithMaxWidth(layout.Current.Container.Width-8),
|
|
modal.WithMaxWidth(layout.Current.Container.Width-8),
|