Session rename
Description
I'd like to rename sessions. Vibe coded a diff. Someone wants to review ?
Session Rename Feature - Implementation Analysis
Current State Analysis
Session Structure (internal/session/session.go):
- Sessions have a
Titlefield (line 18) - There's already a
Save()method (line 113) that updates titles - The database already supports updating session titles
Session Display (internal/tui/components/chat/sidebar/sidebar.go):
- Session title is displayed in the sidebar (lines 138, 140)
- Title is editable in the UI
Session Management (internal/tui/components/dialogs/sessions/sessions.go):
- There's a sessions dialog for switching between sessions
- Current key bindings:
Ctrl+Sopens Commands dialog → "Switch Session"
What Would Need to be Implemented for Session Rename
1. Session Rename Functionality:
- A
Rename(ctx context.Context, id, newTitle string) errormethod in thesession.Serviceinterface - Implementation that calls
session.Save()with new title
2. UI Extensions:
- New key binding in Sessions dialog (e.g.,
F2orCtrl+Rfor Rename) - Rename input dialog when rename key is pressed
- Inline-edit functionality in Sessions list
3. Extended Key Bindings (internal/tui/components/dialogs/sessions/keys.go):
type KeyMap struct {
Select,
Next,
Previous,
Rename // Add new
Close key.Binding
}
4. Rename Handler in Sessions Dialog:
- Input field for new title
- Validation of new title
- Call session rename functionality
- UI updates after successful rename
5. Integration with Session Service:
func (s *sessionDialogCmp) renameSelectedSession() error {
selected := s.sessionsList.SelectedItem()
if selected != nil {
session := selected.Value()
newTitle := /* Input dialog for new title */
return s.sessionService.Rename(ctx, session.ID, newTitle)
}
return nil
}
Implementation Diff Summary
# Session Service Interface Extension
+ Rename(ctx context.Context, id, newTitle string) error
# Sessions KeyMap Extension
+Rename key.Binding (F2/Ctrl+R)
# Session Dialog Component
+ renameInput *textinput.Model
+ handleRenameKeyPress() tea.Cmd
+ handleRenameSubmit(msg RenameSessionMsg) tea.Cmd
+ renderRenameInput() string
# New Messages
+ type RenameSessionMsg struct { NewTitle string }
# Dependencies
+ "charm.land/bubbles/v2/textinput"
Technical Implementation
The existing infrastructure already supports session updates through:
session.Save()method- Pub/Sub events for session updates
- Database updates via
db.UpdateSession()
So only a relatively small extension for rename functionality needs to be implemented, since the foundation is already in place.
Required Components Summary:
- New Method:
Rename(ctx context.Context, id, newTitle string)in Session Service - New Key Binding: F2/Ctrl+R for Rename
- Rename Input UI: Text input component for new title
- Rename Handler: Logic to rename and refresh UI
- Session Service Access: Access to Session Service in dialog
- UI State Management: Distinction between Rename mode and normal mode
The implementation would leverage the existing session infrastructure (Save, Update, Pub/Sub) and only extend the UI interaction with rename functionality.
I think we should probally aim for a session managment system that lets us do thing like:
- rename
- delete
- duplicate
Sounds good. Here a new diff:
Complete Session Management - Implementation Diff
1. Session Service Interface Complete CRUD Extension
--- a/internal/session/session.go
+++ b/internal/session/session.go
@@ -28,6 +28,8 @@ type Service interface {
Create(ctx context.Context, title string) (Session, error)
CreateTitleSession(ctx context.Context, parentSessionID string) (Session, error)
CreateTaskSession(ctx context.Context, toolCallID, parentSessionID, title string) (Session, error)
+ Rename(ctx context.Context, id, newTitle string) error
+ Duplicate(ctx context.Context, id string) (Session, error)
Get(ctx context.Context, id string) (Session, error)
List(ctx context.Context) ([]Session, error)
Save(ctx context.Context, session Session) (Session, error)
@@ -90,6 +92,22 @@ func (s *service) CreateTitleSession(ctx context.Context, parentSessionID strin
return session, nil
}
+func (s *service) Rename(ctx context.Context, id, newTitle string) error {
+ existing, err := s.Get(ctx, id)
+ if err != nil {
+ return err
+ }
+ _, err = s.Save(ctx, Session{...existing, Title: newTitle})
+ return err
+}
+
+func (s *service) Duplicate(ctx context.Context, id string) (Session, error) {
+ original, err := s.Get(ctx, id)
+ if err != nil {
+ return Session{}, err
+ }
+ newTitle := "Copy of " + original.Title
+ return s.Create(ctx, newTitle)
+}
+
func (s *service) Delete(ctx context.Context, id string) error {
2. Sessions KeyMap Complete Extension
--- a/internal/tui/components/dialogs/sessions/keys.go
+++ b/internal/tui/components/dialogs/sessions/keys.go
@@ -7,6 +7,8 @@ type KeyMap struct {
Select,
Next,
Previous,
+ Rename,
+ Duplicate,
+ Delete,
Close key.Binding
}
@@ -15,6 +17,16 @@ func DefaultKeyMap() KeyMap {
return KeyMap{
Select: key.NewBinding(
key.WithKeys("enter", "tab", "ctrl+y"),
key.WithHelp("enter", "choose"),
+ ),
+ Rename: key.NewBinding(
+ key.WithKeys("f2"),
+ key.WithHelp("f2", "rename"),
+ ),
+ Duplicate: key.NewBinding(
+ key.WithKeys("ctrl+d"),
+ key.WithHelp("ctrl+d", "duplicate"),
+ ),
+ Delete: key.NewBinding(
+ key.WithKeys("del"),
+ key.WithHelp("del", "delete"),
),
Next: key.NewBinding(
key.WithKeys("down", "ctrl+n"),
@@ -37,6 +49,9 @@ func (k KeyMap) KeyBindings() []key.Binding {
return []key.Binding{
k.Select,
k.Next,
+ k.Rename,
+ k.Duplicate,
+ k.Delete,
k.Previous,
k.Close,
}
@@ -58,10 +73,13 @@ func (k KeyMap) FullHelp() [][]key.Binding {
func (k KeyMap) ShortHelp() []key.Binding {
return []key.Binding{
- key.NewBinding(
+ k.Rename,
+ k.Duplicate,
+ k.Delete,
+ key.NewBinding(
key.WithKeys("down", "up"),
key.WithHelp("↑↓", "choose"),
),
- k.Select,
+ k.Select,
k.Close,
}
}
3. Session Dialog Component Complete Extension
--- a/internal/tui/components/dialogs/sessions/sessions.go
+++ b/internal/tui/components/dialogs/sessions/sessions.go
@@ -1,8 +1,11 @@
package sessions
+import "context"
import (
+ "charm.land/bubbles/v2/textinput"
"charm.land/bubbles/v2/help"
"charm.land/bubbles/v2/key"
@@ -9,6 +12,7 @@ import (
"github.com/charmbracelet/crush/internal/event"
"github.com/charmbracelet/crush/internal/session"
"github.com/charmbracelet/crush/internal/tui/components/chat"
+ "github.com/charmbracelet/crush/internal/tui/components/dialogs"
"github.com/charmbracelet/crush/internal/tui/components/core"
"github.com/charmbracelet/crush/internal/tui/components/dialogs"
"github.com/charmbracelet/crush/internal/tui/exp/list"
@@ -26,6 +30,9 @@ type sessionDialogCmp struct {
type sessionDialogCmp struct {
selectedInx int
wWidth int
wHeight int
+ actionInput *textinput.Model
+ showConfirmDelete bool
+ pendingAction ActionType
width int
selectedSessionID string
keyMap KeyMap
sessionsList SessionsList
@@ -96,8 +103,25 @@ func (s *sessionDialogCmp) Update(msg tea.Msg) (util.Model, tea.Cmd) {
case tea.KeyPressMsg:
switch {
+ case key.Matches(msg, s.keyMap.Rename):
+ return s, s.handleActionKeyPress(ActionRename)
+ case key.Matches(msg, s.keyMap.Duplicate):
+ return s, s.handleDuplicateKeyPress()
+ case key.Matches(msg, s.keyMap.Delete):
+ return s, s.handleDeleteKeyPress()
+ case key.Matches(msg, s.keyMap.Close) && s.actionInput != nil:
+ s.actionInput = nil
+ s.pendingAction = NoAction
+ return s, s.sessionsList.Focus()
+ case key.Matches(msg, s.keyMap.Close) && s.showConfirmDelete:
+ s.showConfirmDelete = false
+ return s, s.sessionsList.Focus()
case key.Matches(msg, s.keyMap.Select):
+ if s.pendingAction == ActionRename && s.actionInput != nil {
+ return s, s.handleRenameSubmit()
+ }
+ if s.showConfirmDelete && key.Matches(msg, s.keyMap.Select) {
+ return s, s.handleConfirmDelete()
+ }
selectedItem := s.sessionsList.SelectedItem()
if selectedItem != nil {
selected := *selectedItem
@@ -108,6 +132,14 @@ func (s *sessionDialogCmp) Update(msg tea.Msg) (util.Model, tea.Cmd) {
}
return s, util.CmdHandler(dialogs.CloseDialogMsg{})
+ case key.Matches(msg, s.keyMap.Select) && s.showConfirmDelete:
+ return s, s.handleConfirmDelete()
+ case key.Matches(msg, s.keyMap.Select) && s.actionInput != nil:
+ if s.pendingAction == ActionRename {
+ return s, s.handleRenameSubmit()
+ }
+ return s, s.actionInput.Update(msg)
+ default:
+ if s.actionInput != nil {
+ _, cmd := s.actionInput.Update(msg)
+ return s, cmd
+ }
case key.Matches(msg, s.keyMap.Close):
@@ -118,6 +150,16 @@ func (s *sessionDialogCmp) Update(msg tea.Msg) (util.Model, tea.Cmd) {
return s, cmd
}
case dialogs.CloseDialogMsg{}:
+ s.actionInput = nil
+ s.showConfirmDelete = false
+ s.pendingAction = NoAction
+ return s, util.CmdHandler(dialogs.CloseDialogMsg{})
+ case SessionActionMsg:
+ return s, s.handleSessionAction(msg)
+ case textinput.Blurred:
+ if s.pendingAction == ActionRename {
+ s.actionInput = nil
+ s.pendingAction = NoAction
+ return s, s.sessionsList.Focus()
+ }
return s, nil
}
@@ -127,7 +169,15 @@ func (s *sessionDialogCmp) View() string {
content := lipgloss.JoinVertical(
lipgloss.Left,
t.S().Base.Padding(0, 1, 1, 1).Render(core.Title("Session Management", s.width-4)),
+ func() string {
+ if s.actionInput != nil {
+ return s.renderActionInput()
+ }
+ if s.showConfirmDelete {
+ return s.renderDeleteConfirm()
+ }
+ return ""
+ }(),
listView,
"",
t.S().Base.Width(s.width-2).PaddingLeft(1).AlignHorizontal(lipgloss.Left).Render(s.help.View(s.keyMap)),
@@ -180,3 +230,103 @@ func (s *sessionDialogCmp) Position() (int, int) {
func (s *sessionDialogCmp) ID() dialogs.DialogID {
return SessionsDialogID
}
+
+func (s *sessionDialogCmp) handleActionKeyPress(action ActionType) tea.Cmd {
+ selected := s.sessionsList.SelectedItem()
+ if selected == nil {
+ return nil
+ }
+
+ // Create action input field for rename
+ actionInput := textinput.New()
+ actionInput.SetValue(selected.Value().Title)
+ actionInput.Focus()
+ s.actionInput = &actionInput
+ s.pendingAction = action
+
+ return nil
+}
+
+func (s *sessionDialogCmp) handleRenameSubmit() tea.Cmd {
+ if s.actionInput == nil || s.pendingAction != ActionRename {
+ return nil
+ }
+
+ newTitle := strings.TrimSpace(s.actionInput.Value())
+ selected := s.sessionsList.SelectedItem()
+ if selected != nil && newTitle != "" {
+ session := selected.Value()
+ err := s.sessionService.Rename(context.Background(), session.ID, newTitle)
+ if err == nil {
+ return s.refreshSessionsList()
+ }
+ }
+
+ s.actionInput = nil
+ s.pendingAction = NoAction
+ return s.sessionsList.Focus()
+}
+
+func (s *sessionDialogCmp) handleDuplicateKeyPress() tea.Cmd {
+ selected := s.sessionsList.SelectedItem()
+ if selected == nil {
+ return nil
+ }
+
+ session := selected.Value()
+ _, err := s.sessionService.Duplicate(context.Background(), session.ID)
+ if err == nil {
+ return s.refreshSessionsList()
+ }
+ return nil
+}
+
+func (s *sessionDialogCmp) handleDeleteKeyPress() tea.Cmd {
+ selected := s.sessionsList.SelectedItem()
+ if selected == nil {
+ return nil
+ }
+
+ s.showConfirmDelete = true
+ return nil
+}
+
+func (s *sessionDialogCmp) handleConfirmDelete() tea.Cmd {
+ if !s.showConfirmDelete {
+ return nil
+ }
+
+ selected := s.sessionsList.SelectedItem()
+ if selected != nil {
+ session := selected.Value()
+ err := s.sessionService.Delete(context.Background(), session.ID)
+ if err == nil {
+ s.showConfirmDelete = false
+ return s.refreshSessionsList()
+ }
+ }
+
+ s.showConfirmDelete = false
+ return s.sessionsList.Focus()
+}
+
+func (s *sessionDialogCmp) refreshSessionsList() tea.Cmd {
+ return func() tea.Msg {
+ sessions, _ := s.sessionService.List(context.Background())
+ items := make([]list.CompletionItem[session.Session], len(sessions))
+ for i, session := range sessions {
+ items[i] = list.NewCompletionItem(session.Title, session, list.WithCompletionID(session.ID))
+ }
+ s.sessionsList.SetItems(items)
+ s.actionInput = nil
+ s.pendingAction = NoAction
+ s.showConfirmDelete = false
+ return nil
+ }
+}
+
+func (s *sessionDialogCmp) renderActionInput() string {
+ if s.actionInput == nil {
+ return ""
+ }
+
+ t := styles.CurrentTheme()
+ return t.S().Base.
+ Width(s.listWidth()).
+ Padding(1).
+ Render("New session name:")
+ }
+
+func (s *sessionDialogCmp) renderDeleteConfirm() string {
+ selected := s.sessionsList.SelectedItem()
+ if selected == nil {
+ return ""
+ }
+
+ session := selected.Value()
+ t := styles.CurrentTheme()
+ confirmMsg := fmt.Sprintf("Delete session '%s'?", session.Title)
+
+ return t.S().Base.
+ Width(s.listWidth()).
+ Padding(1).
+ Render(confirmMsg)
+}
+
+type ActionType int
+
+const (
+ NoAction ActionType = iota
+ ActionRename
+)
+
+type SessionActionMsg struct {
+ Action ActionType
+ Title string
+}
4. Dependencies Extension
--- a/internal/tui/components/dialogs/sessions/sessions.go
+++ b/internal/tui/components/dialogs/sessions/sessions.go
@@ -1,6 +1,7 @@
package sessions
+import "context"
import (
+ "fmt"
+ "strings"
+ "charm.land/bubbles/v2/textinput"
"charm.land/bubbles/v2/help"
5. Integration Summary
# Complete Session Management Features:
+ Rename (F2): Rename selected session
+ Ctrl+D: Duplicate selected session
+ Delete: Delete selected session with confirmation
+ Enter: Confirm action (save rename, confirm delete)
# New State Management:
+ actionInput: Text input for rename operations
+ showConfirmDelete: Confirmation dialog state
+ pendingAction: Current action type (rename/none)
# New UI Components:
+ Action input rendering
+ Delete confirmation dialog
+ Action-specific key handling
# Error Handling:
+ Operation validation (empty titles, failed operations)
+ State cleanup on operation completion
+ Graceful error handling for all CRUD operations
# Session Service Integration:
+ All operations use existing session service
+ Leverage existing pub/sub infrastructure
+ Automatic UI refresh after operations
UX Flow Examples
Rename Flow:
- User selects session
- Presses F2
- Input field appears with current title
- User edits title and presses Enter
- Session is renamed and list refreshes
Delete Flow:
- User selects session
- Presses Delete key
- Confirmation dialog appears
- User presses Enter to confirm
- Session is deleted and list refreshes
Duplicate Flow:
- User selects session
- Presses Ctrl+D
- New session "Copy of [original]" is created
- List refreshes showing new session
Technical Benefits
- Complete CRUD: Full session lifecycle management
- Consistent UX: Standard keyboard shortcuts and confirmation patterns
- Data Safety: Delete confirmation prevents accidental data loss
- User Productivity: Quick session organization and management
- Leverage Existing: Builds on proven session infrastructure
- Real-time Updates: Pub/Sub integration keeps UI synchronized
This implementation provides a comprehensive session management system while maintaining consistency with Crush's existing patterns and leveraging the robust session infrastructure already in place.