Fix (race): mutex chatbody

This commit is contained in:
Grail Finder
2026-03-07 10:46:18 +03:00
parent 014e297ae3
commit a842b00e96
9 changed files with 422 additions and 142 deletions

56
tui.go
View File

@@ -355,7 +355,7 @@ func init() {
searchResults = nil // Clear search results
searchResultLengths = nil // Clear search result lengths
originalTextForSearch = ""
textView.SetText(chatToText(chatBody.Messages, cfg.ShowSys)) // Reset text without search regions
textView.SetText(chatToText(chatBody.GetMessages(), cfg.ShowSys)) // Reset text without search regions
colorText() // Apply normal chat coloring
} else {
// Original logic if no search is active
@@ -436,9 +436,11 @@ func init() {
pages.RemovePage(editMsgPage)
return nil
}
chatBody.Messages[selectedIndex].SetText(editedMsg)
chatBody.WithLock(func(cb *models.ChatBody) {
cb.Messages[selectedIndex].SetText(editedMsg)
})
// change textarea
textView.SetText(chatToText(chatBody.Messages, cfg.ShowSys))
textView.SetText(chatToText(chatBody.GetMessages(), cfg.ShowSys))
pages.RemovePage(editMsgPage)
editMode = false
return nil
@@ -466,9 +468,11 @@ func init() {
pages.RemovePage(roleEditPage)
return
}
if selectedIndex >= 0 && selectedIndex < len(chatBody.Messages) {
chatBody.Messages[selectedIndex].Role = newRole
textView.SetText(chatToText(chatBody.Messages, cfg.ShowSys))
if selectedIndex >= 0 && selectedIndex < chatBody.GetMessageCount() {
chatBody.WithLock(func(cb *models.ChatBody) {
cb.Messages[selectedIndex].Role = newRole
})
textView.SetText(chatToText(chatBody.GetMessages(), cfg.ShowSys))
colorText()
pages.RemovePage(roleEditPage)
}
@@ -497,7 +501,7 @@ func init() {
return nil
}
selectedIndex = siInt
if len(chatBody.Messages)-1 < selectedIndex || selectedIndex < 0 {
if chatBody.GetMessageCount()-1 < selectedIndex || selectedIndex < 0 {
msg := "chosen index is out of bounds, will copy user input"
logger.Warn(msg, "index", selectedIndex)
showToast("error", msg)
@@ -507,7 +511,7 @@ func init() {
hideIndexBar() // Hide overlay instead of removing page directly
return nil
}
m := chatBody.Messages[selectedIndex]
m := chatBody.GetMessages()[selectedIndex]
switch {
case roleEditMode:
hideIndexBar() // Hide overlay first
@@ -574,7 +578,7 @@ func init() {
searchResults = nil
searchResultLengths = nil
originalTextForSearch = ""
textView.SetText(chatToText(chatBody.Messages, cfg.ShowSys))
textView.SetText(chatToText(chatBody.GetMessages(), cfg.ShowSys))
colorText()
return
} else {
@@ -632,7 +636,7 @@ func init() {
//
textArea.SetMovedFunc(updateStatusLine)
updateStatusLine()
textView.SetText(chatToText(chatBody.Messages, cfg.ShowSys))
textView.SetText(chatToText(chatBody.GetMessages(), cfg.ShowSys))
colorText()
if scrollToEndEnabled {
textView.ScrollToEnd()
@@ -646,7 +650,7 @@ func init() {
if event.Key() == tcell.KeyRune && event.Rune() == '5' && event.Modifiers()&tcell.ModAlt != 0 {
// switch cfg.ShowSys
cfg.ShowSys = !cfg.ShowSys
textView.SetText(chatToText(chatBody.Messages, cfg.ShowSys))
textView.SetText(chatToText(chatBody.GetMessages(), cfg.ShowSys))
colorText()
}
if event.Key() == tcell.KeyRune && event.Rune() == '3' && event.Modifiers()&tcell.ModAlt != 0 {
@@ -679,7 +683,7 @@ func init() {
// Handle Alt+T to toggle thinking block visibility
if event.Key() == tcell.KeyRune && event.Rune() == 't' && event.Modifiers()&tcell.ModAlt != 0 {
thinkingCollapsed = !thinkingCollapsed
textView.SetText(chatToText(chatBody.Messages, cfg.ShowSys))
textView.SetText(chatToText(chatBody.GetMessages(), cfg.ShowSys))
colorText()
status := "expanded"
if thinkingCollapsed {
@@ -691,7 +695,7 @@ func init() {
// Handle Ctrl+T to toggle tool call/response visibility
if event.Key() == tcell.KeyCtrlT {
toolCollapsed = !toolCollapsed
textView.SetText(chatToText(chatBody.Messages, cfg.ShowSys))
textView.SetText(chatToText(chatBody.GetMessages(), cfg.ShowSys))
colorText()
status := "expanded"
if toolCollapsed {
@@ -734,14 +738,14 @@ func init() {
}
if event.Key() == tcell.KeyF2 && !botRespMode {
// regen last msg
if len(chatBody.Messages) == 0 {
if chatBody.GetMessageCount() == 0 {
showToast("info", "no messages to regenerate")
return nil
}
chatBody.Messages = chatBody.Messages[:len(chatBody.Messages)-1]
chatBody.TruncateMessages(chatBody.GetMessageCount() - 1)
// there is no case where user msg is regenerated
// lastRole := chatBody.Messages[len(chatBody.Messages)-1].Role
textView.SetText(chatToText(chatBody.Messages, cfg.ShowSys))
// lastRole := chatBody.GetMessages()[chatBody.GetMessageCount()-1].Role
textView.SetText(chatToText(chatBody.GetMessages(), cfg.ShowSys))
// go chatRound("", cfg.UserRole, textView, true, false)
if cfg.TTS_ENABLED {
TTSDoneChan <- true
@@ -760,12 +764,12 @@ func init() {
colorText()
return nil
}
if len(chatBody.Messages) == 0 {
if chatBody.GetMessageCount() == 0 {
showToast("info", "no messages to delete")
return nil
}
chatBody.Messages = chatBody.Messages[:len(chatBody.Messages)-1]
textView.SetText(chatToText(chatBody.Messages, cfg.ShowSys))
chatBody.TruncateMessages(chatBody.GetMessageCount() - 1)
textView.SetText(chatToText(chatBody.GetMessages(), cfg.ShowSys))
if cfg.TTS_ENABLED {
TTSDoneChan <- true
}
@@ -813,7 +817,7 @@ func init() {
if event.Key() == tcell.KeyF7 {
// copy msg to clipboard
editMode = false
m := chatBody.Messages[len(chatBody.Messages)-1]
m := chatBody.GetMessages()[chatBody.GetMessageCount()-1]
msgText := m.GetText()
if err := copyToClipboard(msgText); err != nil {
logger.Error("failed to copy to clipboard", "error", err)
@@ -997,10 +1001,10 @@ func init() {
TTSDoneChan <- true
}
if event.Key() == tcell.KeyRune && event.Rune() == '0' && event.Modifiers()&tcell.ModAlt != 0 && cfg.TTS_ENABLED {
if len(chatBody.Messages) > 0 {
if chatBody.GetMessageCount() > 0 {
// Stop any currently playing TTS first
TTSDoneChan <- true
lastMsg := chatBody.Messages[len(chatBody.Messages)-1]
lastMsg := chatBody.GetMessages()[chatBody.GetMessageCount()-1]
cleanedText := models.CleanText(lastMsg.GetText())
if cleanedText != "" {
// nolint: errcheck
@@ -1012,7 +1016,7 @@ func init() {
if event.Key() == tcell.KeyCtrlW {
// INFO: continue bot/text message
// without new role
lastRole := chatBody.Messages[len(chatBody.Messages)-1].Role
lastRole := chatBody.GetMessages()[chatBody.GetMessageCount()-1].Role
// go chatRound("", lastRole, textView, false, true)
chatRoundChan <- &models.ChatRoundReq{Role: lastRole, Resume: true}
return nil
@@ -1098,7 +1102,7 @@ func init() {
if event.Key() == tcell.KeyRune && event.Modifiers() == tcell.ModAlt && event.Rune() == '9' {
// Warm up (load) the currently selected model
go warmUpModel()
showToast("model warmup", "loading model: "+chatBody.Model)
showToast("model warmup", "loading model: "+chatBody.GetModel())
return nil
}
// cannot send msg in editMode or botRespMode
@@ -1137,7 +1141,7 @@ func init() {
}
// add user icon before user msg
fmt.Fprintf(textView, "%s[-:-:b](%d) <%s>: [-:-:-]\n%s\n",
nl, len(chatBody.Messages), persona, msgText)
nl, chatBody.GetMessageCount(), persona, msgText)
textArea.SetText("", true)
if scrollToEndEnabled {
textView.ScrollToEnd()