summaryrefslogtreecommitdiffhomepage
path: root/packages/tui
diff options
context:
space:
mode:
authorspoons-and-mirrors <[email protected]>2025-08-12 22:22:03 +0200
committerGitHub <[email protected]>2025-08-12 16:22:03 -0400
commit47c327641b2beec66fc3960136634d5490ba6bb3 (patch)
treeb5c40f4824495db0efe50fc72531fc669a86b5db /packages/tui
parent81583cddbdd588fa3eb9e3e15ea70909ce1b4b93 (diff)
downloadopencode-47c327641b2beec66fc3960136634d5490ba6bb3.tar.gz
opencode-47c327641b2beec66fc3960136634d5490ba6bb3.zip
feat: add session rename functionality to TUI modal (#1821)
Co-authored-by: opencode <[email protected]> Co-authored-by: Dax Raad <[email protected]> Co-authored-by: Dax <[email protected]>
Diffstat (limited to 'packages/tui')
-rw-r--r--packages/tui/internal/app/app.go11
-rw-r--r--packages/tui/internal/components/dialog/session.go195
-rw-r--r--packages/tui/internal/tui/tui.go5
3 files changed, 170 insertions, 41 deletions
diff --git a/packages/tui/internal/app/app.go b/packages/tui/internal/app/app.go
index 345726483..48b7d67f9 100644
--- a/packages/tui/internal/app/app.go
+++ b/packages/tui/internal/app/app.go
@@ -760,6 +760,17 @@ func (a *App) DeleteSession(ctx context.Context, sessionID string) error {
return nil
}
+func (a *App) UpdateSession(ctx context.Context, sessionID string, title string) error {
+ _, err := a.Client.Session.Update(ctx, sessionID, opencode.SessionUpdateParams{
+ Title: opencode.F(title),
+ })
+ if err != nil {
+ slog.Error("Failed to update session", "error", err)
+ return err
+ }
+ return nil
+}
+
func (a *App) ListMessages(ctx context.Context, sessionId string) ([]Message, error) {
response, err := a.Client.Session.Messages(ctx, sessionId)
if err != nil {
diff --git a/packages/tui/internal/components/dialog/session.go b/packages/tui/internal/components/dialog/session.go
index daf7a142b..08ff22a22 100644
--- a/packages/tui/internal/components/dialog/session.go
+++ b/packages/tui/internal/components/dialog/session.go
@@ -6,6 +6,7 @@ import (
"slices"
+ "github.com/charmbracelet/bubbles/v2/textinput"
tea "github.com/charmbracelet/bubbletea/v2"
"github.com/muesli/reflow/truncate"
"github.com/sst/opencode-sdk-go"
@@ -110,6 +111,9 @@ type sessionDialog struct {
list list.List[sessionItem]
app *app.App
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 {
@@ -123,69 +127,128 @@ func (s *sessionDialog) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
s.height = msg.Height
s.list.SetMaxWidth(layout.Current.Container.Width - 12)
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()
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(
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()
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 {
+ 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()
t := theme.CurrentTheme()
keyStyle := styles.NewStyle().Foreground(t.Text()).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")
bgColor := t.BackgroundPanel()
@@ -203,6 +266,39 @@ func (s *sessionDialog) Render(background string) string {
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() {
_, 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 {
+ 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
}
@@ -272,6 +383,8 @@ func NewSessionDialog(app *app.App) SessionDialog {
list: listComponent,
app: app,
deleteConfirmation: -1,
+ renameMode: false,
+ renameIndex: -1,
modal: modal.New(
modal.WithTitle("Switch Session"),
modal.WithMaxWidth(layout.Current.Container.Width-8),
diff --git a/packages/tui/internal/tui/tui.go b/packages/tui/internal/tui/tui.go
index 6b91cbfeb..0262f5077 100644
--- a/packages/tui/internal/tui/tui.go
+++ b/packages/tui/internal/tui/tui.go
@@ -357,6 +357,11 @@ func (a Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
}
a.modal = nil
return a, cmd
+ case dialog.ReopenSessionModalMsg:
+ // Reopen the session modal (used when exiting rename mode)
+ sessionDialog := dialog.NewSessionDialog(a.app)
+ a.modal = sessionDialog
+ return a, nil
case commands.ExecuteCommandMsg:
updated, cmd := a.executeCommand(commands.Command(msg))
return updated, cmd