Detailed changes
@@ -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 {
@@ -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)
@@ -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)
@@ -45,6 +45,7 @@ type ActionSelectModel struct {
type (
ActionNewSession struct{}
ActionForkConversation struct{}
+ ActionDeleteMessages struct{}
ActionToggleHelp struct{}
ActionToggleCompactMode struct{}
ActionToggleThinking struct{}
@@ -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}),
@@ -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)
}