diff --git a/bot.go b/bot.go index 2d6af0c..bdf71b9 100644 --- a/bot.go +++ b/bot.go @@ -119,7 +119,7 @@ func processMessageTag(msg *models.RoleMsg) *models.RoleMsg { } // If KnownTo already set, assume tag already processed (content cleaned). // However, we still check for new tags (maybe added later). - knownTo := parseKnownToTag(msg.Content) + knownTo := parseKnownToTag(msg.GetText()) // If tag found, replace KnownTo with new list (merge with existing?) // For simplicity, if knownTo is not nil, replace. if knownTo == nil { @@ -1303,12 +1303,9 @@ func removeThinking(chatBody *models.ChatBody) { if msg.Role == cfg.ToolRole { continue } - // find thinking and remove it - rm := models.RoleMsg{ - Role: msg.Role, - Content: thinkRE.ReplaceAllString(msg.Content, ""), - } - msgs = append(msgs, rm) + // find thinking and remove it - use SetText to preserve ContentParts + msg.SetText(thinkRE.ReplaceAllString(msg.GetText(), "")) + msgs = append(msgs, msg) } chatBody.Messages = msgs } diff --git a/helpfuncs.go b/helpfuncs.go index 8719aab..99024a0 100644 --- a/helpfuncs.go +++ b/helpfuncs.go @@ -75,15 +75,16 @@ func stripThinkingFromMsg(msg *models.RoleMsg) *models.RoleMsg { if !cfg.StripThinkingFromAPI { return msg } - // Skip user, tool, and system messages - they might contain thinking examples + // Skip user, tool, they might contain thinking and system messages - examples if msg.Role == cfg.UserRole || msg.Role == cfg.ToolRole || msg.Role == "system" { return msg } // Strip thinking from assistant messages - if thinkRE.MatchString(msg.Content) { - msg.Content = thinkRE.ReplaceAllString(msg.Content, "") - // Clean up any double newlines that might result - msg.Content = strings.TrimSpace(msg.Content) + msgText := msg.GetText() + if thinkRE.MatchString(msgText) { + cleanedText := thinkRE.ReplaceAllString(msgText, "") + cleanedText = strings.TrimSpace(cleanedText) + msg.SetText(cleanedText) } return msg } diff --git a/models/models.go b/models/models.go index ee13928..21d71d8 100644 --- a/models/models.go +++ b/models/models.go @@ -329,6 +329,66 @@ func (m *RoleMsg) Copy() RoleMsg { } } +// GetText returns the text content of the message, handling both +// simple Content and multimodal ContentParts formats. +func (m *RoleMsg) GetText() string { + if !m.hasContentParts { + return m.Content + } + var textParts []string + for _, part := range m.ContentParts { + switch p := part.(type) { + case TextContentPart: + if p.Type == "text" { + textParts = append(textParts, p.Text) + } + case map[string]any: + if partType, exists := p["type"]; exists { + if partType == "text" { + if textVal, textExists := p["text"]; textExists { + if textStr, isStr := textVal.(string); isStr { + textParts = append(textParts, textStr) + } + } + } + } + } + } + return strings.Join(textParts, " ") +} + +// SetText updates the text content of the message. If the message has +// ContentParts (multimodal), it updates the text parts while preserving +// images. If not, it sets the simple Content field. +func (m *RoleMsg) SetText(text string) { + if !m.hasContentParts { + m.Content = text + return + } + var newParts []any + for _, part := range m.ContentParts { + switch p := part.(type) { + case TextContentPart: + if p.Type == "text" { + p.Text = text + newParts = append(newParts, p) + } else { + newParts = append(newParts, p) + } + case map[string]any: + if partType, exists := p["type"]; exists && partType == "text" { + p["text"] = text + newParts = append(newParts, p) + } else { + newParts = append(newParts, p) + } + default: + newParts = append(newParts, part) + } + } + m.ContentParts = newParts +} + // AddTextPart adds a text content part to the message func (m *RoleMsg) AddTextPart(text string) { if !m.hasContentParts { diff --git a/tui.go b/tui.go index ca40c58..ddddd35 100644 --- a/tui.go +++ b/tui.go @@ -264,7 +264,7 @@ func init() { pages.RemovePage(editMsgPage) return nil } - chatBody.Messages[selectedIndex].Content = editedMsg + chatBody.Messages[selectedIndex].SetText(editedMsg) // change textarea textView.SetText(chatToText(chatBody.Messages, cfg.ShowSys)) pages.RemovePage(editMsgPage) @@ -352,13 +352,14 @@ func init() { case editMode: hideIndexBar() // Hide overlay first pages.AddPage(editMsgPage, editArea, true, true) - editArea.SetText(m.Content, true) + editArea.SetText(m.GetText(), true) default: - if err := copyToClipboard(m.Content); err != nil { + msgText := m.GetText() + if err := copyToClipboard(msgText); err != nil { logger.Error("failed to copy to clipboard", "error", err) } - previewLen := min(30, len(m.Content)) - notification := fmt.Sprintf("msg '%s' was copied to the clipboard", m.Content[:previewLen]) + previewLen := min(30, len(msgText)) + notification := fmt.Sprintf("msg '%s' was copied to the clipboard", msgText[:previewLen]) if err := notifyUser("copied", notification); err != nil { logger.Error("failed to send notification", "error", err) } @@ -648,11 +649,12 @@ func init() { // copy msg to clipboard editMode = false m := chatBody.Messages[len(chatBody.Messages)-1] - if err := copyToClipboard(m.Content); err != nil { + msgText := m.GetText() + if err := copyToClipboard(msgText); err != nil { logger.Error("failed to copy to clipboard", "error", err) } - previewLen := min(30, len(m.Content)) - notification := fmt.Sprintf("msg '%s' was copied to the clipboard", m.Content[:previewLen]) + previewLen := min(30, len(msgText)) + notification := fmt.Sprintf("msg '%s' was copied to the clipboard", msgText[:previewLen]) if err := notifyUser("copied", notification); err != nil { logger.Error("failed to send notification", "error", err) } @@ -847,7 +849,7 @@ func init() { // Stop any currently playing TTS first TTSDoneChan <- true lastMsg := chatBody.Messages[len(chatBody.Messages)-1] - cleanedText := models.CleanText(lastMsg.Content) + cleanedText := models.CleanText(lastMsg.GetText()) if cleanedText != "" { // nolint: errcheck go orator.Speak(cleanedText)