diff --git a/internal/message/message.go b/internal/message/message.go index 02309fae70ed7f7a59d151797cc5198a0ee92550..405148df0e57c0ccd189c526da05a7348c70a148 100644 --- a/internal/message/message.go +++ b/internal/message/message.go @@ -31,6 +31,7 @@ type Service interface { Copy(ctx context.Context, sessionID string, message Message) (Message, error) Delete(ctx context.Context, id string) error DeleteSessionMessages(ctx context.Context, sessionID string) error + DeleteMessagesFrom(ctx context.Context, sessionID, messageID string) error } type service struct { @@ -123,6 +124,29 @@ func (s *service) DeleteSessionMessages(ctx context.Context, sessionID string) e return nil } +func (s *service) DeleteMessagesFrom(ctx context.Context, sessionID, messageID string) error { + allMessages, err := s.List(ctx, sessionID) + if err != nil { + return err + } + targetIndex := -1 + for i, msg := range allMessages { + if msg.ID == messageID { + targetIndex = i + break + } + } + if targetIndex == -1 { + return fmt.Errorf("message not found: %s", messageID) + } + for i := len(allMessages) - 1; i >= targetIndex; i-- { + if err := s.Delete(ctx, allMessages[i].ID); err != nil { + return err + } + } + return nil +} + func (s *service) Update(ctx context.Context, message Message) error { parts, err := marshalParts(message.Parts) if err != nil { diff --git a/internal/session/session.go b/internal/session/session.go index c7574aee4ce1c5dccf7f88dd7a09763e34af75f7..37466a00c91a80455556d3590988299d1ab3348b 100644 --- a/internal/session/session.go +++ b/internal/session/session.go @@ -173,7 +173,7 @@ func (s *service) Fork(ctx context.Context, sourceSessionID, upToMessageID strin return Session{}, fmt.Errorf("creating session: %w", err) } - for i := 0; i <= targetIndex; i++ { + for i := 0; i < targetIndex; i++ { _, err = messageSvc.Copy(ctx, newSession.ID, messages[i]) if err != nil { _ = s.Delete(ctx, newSession.ID) diff --git a/internal/session/session_fork_test.go b/internal/session/session_fork_test.go index c0710aa47a97020dd6f5f6d13b2d0080dc8abbef..8712ab63effdc4fb87824e38d2e6c6351a00e73a 100644 --- a/internal/session/session_fork_test.go +++ b/internal/session/session_fork_test.go @@ -53,7 +53,7 @@ func TestFork(t *testing.T) { require.Contains(t, newSession.Title, sourceSession.Title) forkedMessages := getMessages(newSession.ID) - require.Len(t, forkedMessages, 3) + require.Len(t, forkedMessages, 2) for i, msg := range forkedMessages { require.Equal(t, sourceMessages[i].Role, msg.Role) diff --git a/internal/ui/dialog/actions.go b/internal/ui/dialog/actions.go index e22641a780bb4c8ab8ca69ed6e75fe11d38e0576..b01d0639f51b4d3b62e8efa91ba8fd97c762cc96 100644 --- a/internal/ui/dialog/actions.go +++ b/internal/ui/dialog/actions.go @@ -45,6 +45,7 @@ type ActionSelectModel struct { type ( ActionNewSession struct{} ActionForkConversation struct{} + ActionDeleteMessages struct{} ActionToggleHelp struct{} ActionToggleCompactMode struct{} ActionToggleThinking struct{} diff --git a/internal/ui/dialog/commands.go b/internal/ui/dialog/commands.go index 48ed977077f3f98a66017085066ece7d1d7786cc..e1da440d24a6933b4fea4b47f12160eee5e8c603 100644 --- a/internal/ui/dialog/commands.go +++ b/internal/ui/dialog/commands.go @@ -66,12 +66,13 @@ type Commands struct { customCommands []commands.CustomCommand mcpPrompts []commands.MCPPrompt canFork bool // whether a user message is selected for forking + canDelete bool // whether a user message is selected for deletion } var _ Dialog = (*Commands)(nil) // NewCommands creates a new commands dialog. -func NewCommands(com *common.Common, sessionID string, customCommands []commands.CustomCommand, mcpPrompts []commands.MCPPrompt, canFork bool) (*Commands, error) { +func NewCommands(com *common.Common, sessionID string, customCommands []commands.CustomCommand, mcpPrompts []commands.MCPPrompt, canFork, canDelete bool) (*Commands, error) { c := &Commands{ com: com, selected: SystemCommands, @@ -79,6 +80,7 @@ func NewCommands(com *common.Common, sessionID string, customCommands []commands customCommands: customCommands, mcpPrompts: mcpPrompts, canFork: canFork, + canDelete: canDelete, } help := help.New() @@ -396,6 +398,12 @@ func (c *Commands) defaultCommands() []*CommandItem { ) } + if c.canDelete { + commands = append(commands, + NewCommandItem(c.com.Styles, "delete_messages", "Delete Messages (including this)", "", ActionDeleteMessages{}), + ) + } + commands = append(commands, NewCommandItem(c.com.Styles, "switch_session", "Sessions", "ctrl+s", ActionOpenDialog{SessionsID}), NewCommandItem(c.com.Styles, "switch_model", "Switch Model", "ctrl+l", ActionOpenDialog{ModelsID}), diff --git a/internal/ui/model/ui.go b/internal/ui/model/ui.go index 0c8276bc03afebd5446049cb6c6b4c67d846b803..7b8a8797edd6ff741b46517cdd51d5c0f2e16b03 100644 --- a/internal/ui/model/ui.go +++ b/internal/ui/model/ui.go @@ -1193,6 +1193,44 @@ func (m *UI) handleDialogMsg(msg tea.Msg) tea.Cmd { } }) m.dialog.CloseDialog(dialog.CommandsID) + m.chat.list.Blur() + m.focus = uiFocusEditor + cmds = append(cmds, m.textarea.Focus()) + + case dialog.ActionDeleteMessages: + if m.isAgentBusy() { + cmds = append(cmds, uiutil.ReportWarn("Agent is busy, please wait...")) + break + } + if m.session == nil { + cmds = append(cmds, uiutil.ReportWarn("No session to delete from...")) + break + } + selectedItem := m.chat.SelectedItem() + if selectedItem == nil { + cmds = append(cmds, uiutil.ReportWarn("No message selected...")) + break + } + if _, ok := selectedItem.(*chat.UserMessageItem); !ok { + cmds = append(cmds, uiutil.ReportWarn("Can only delete from user messages...")) + break + } + messageID, ok := m.getMessageIDFromItem(selectedItem) + if !ok { + cmds = append(cmds, uiutil.ReportWarn("Cannot get message ID from selected item...")) + break + } + cmds = append(cmds, func() tea.Msg { + err := m.com.App.Messages.DeleteMessagesFrom(context.Background(), m.session.ID, messageID) + if err != nil { + return uiutil.ReportError(err)() + } + return m.loadSession(m.session.ID)() + }) + m.dialog.CloseDialog(dialog.CommandsID) + m.chat.list.Blur() + m.focus = uiFocusEditor + cmds = append(cmds, m.textarea.Focus()) case dialog.ActionSelectModel: if m.isAgentBusy() { @@ -2752,10 +2790,12 @@ func (m *UI) openModelsDialog() tea.Cmd { // openCommandsDialog opens the commands dialog. func (m *UI) openCommandsDialog() tea.Cmd { canFork := false + canDelete := false selectedItem := m.chat.SelectedItem() if selectedItem != nil { if _, ok := selectedItem.(*chat.UserMessageItem); ok { canFork = true + canDelete = true } } @@ -2769,7 +2809,7 @@ func (m *UI) openCommandsDialog() tea.Cmd { sessionID = m.session.ID } - commands, err := dialog.NewCommands(m.com, sessionID, m.customCommands, m.mcpPrompts, canFork) + commands, err := dialog.NewCommands(m.com, sessionID, m.customCommands, m.mcpPrompts, canFork, canDelete) if err != nil { return uiutil.ReportError(err) }