Tweaks/cleanups to conversation management in tui
- Pass around message/conversation values instead of pointers where it makes more sense, and store values instead of pointers in the globally (within the TUI) shared `App` (pointers provide no utility here). - Split conversation persistence into separate conversation/message saving stages
This commit is contained in:
@@ -4,8 +4,8 @@ import (
|
||||
"time"
|
||||
|
||||
"git.mlow.ca/mlow/lmcli/pkg/api"
|
||||
"git.mlow.ca/mlow/lmcli/pkg/provider"
|
||||
"git.mlow.ca/mlow/lmcli/pkg/conversation"
|
||||
"git.mlow.ca/mlow/lmcli/pkg/provider"
|
||||
"git.mlow.ca/mlow/lmcli/pkg/tui/model"
|
||||
"github.com/charmbracelet/bubbles/cursor"
|
||||
"github.com/charmbracelet/bubbles/spinner"
|
||||
@@ -20,10 +20,8 @@ type (
|
||||
// sent when a new conversation title generated
|
||||
msgConversationTitleGenerated string
|
||||
// sent when the conversation has been persisted, triggers a reload of contents
|
||||
msgConversationPersisted struct {
|
||||
conversation *conversation.Conversation
|
||||
messages []conversation.Message
|
||||
}
|
||||
msgConversationPersisted conversation.Conversation
|
||||
msgMessagesPersisted []conversation.Message
|
||||
// sent when a conversation's messages are laoded
|
||||
msgConversationMessagesLoaded struct {
|
||||
messages []conversation.Message
|
||||
@@ -35,7 +33,7 @@ type (
|
||||
// sent on each chunk received from LLM
|
||||
msgChatResponseChunk provider.Chunk
|
||||
// sent on each completed reply
|
||||
msgChatResponse *conversation.Message
|
||||
msgChatResponse conversation.Message
|
||||
// sent when the response is canceled
|
||||
msgChatResponseCanceled struct{}
|
||||
// sent when results from a tool call are returned
|
||||
|
||||
@@ -36,16 +36,6 @@ func (m *Model) generateConversationTitle() tea.Cmd {
|
||||
}
|
||||
}
|
||||
|
||||
func (m *Model) updateConversationTitle(conversation *conversation.Conversation) tea.Cmd {
|
||||
return func() tea.Msg {
|
||||
err := m.App.UpdateConversationTitle(conversation)
|
||||
if err != nil {
|
||||
return shared.WrapError(err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func (m *Model) cloneMessage(message conversation.Message, selected bool) tea.Cmd {
|
||||
return func() tea.Msg {
|
||||
msg, err := m.App.CloneMessage(message, selected)
|
||||
@@ -96,11 +86,21 @@ func (m *Model) cycleSelectedReply(message *conversation.Message, dir model.Mess
|
||||
|
||||
func (m *Model) persistConversation() tea.Cmd {
|
||||
return func() tea.Msg {
|
||||
conversation, messages, err := m.App.PersistConversation(m.App.Conversation, m.App.Messages)
|
||||
conversation, err := m.App.PersistConversation()
|
||||
if err != nil {
|
||||
return shared.AsMsgError(err)
|
||||
}
|
||||
return msgConversationPersisted{conversation, messages}
|
||||
return msgConversationPersisted(conversation)
|
||||
}
|
||||
}
|
||||
|
||||
func (m *Model) persistMessages() tea.Cmd {
|
||||
return func() tea.Msg {
|
||||
messages, err := m.App.PersistMessages()
|
||||
if err != nil {
|
||||
return shared.AsMsgError(err)
|
||||
}
|
||||
return msgMessagesPersisted(messages)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -130,7 +130,7 @@ func (m *Model) promptLLM() tea.Cmd {
|
||||
if err != nil {
|
||||
return msgChatResponseError{Err: err}
|
||||
}
|
||||
return msgChatResponse(resp)
|
||||
return msgChatResponse(*resp)
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
@@ -127,7 +127,7 @@ func (m *Model) handleMessagesKey(msg tea.KeyMsg) tea.Cmd {
|
||||
|
||||
var cmd tea.Cmd
|
||||
if m.selectedMessage == 0 {
|
||||
cmd = m.cycleSelectedRoot(m.App.Conversation, dir)
|
||||
cmd = m.cycleSelectedRoot(&m.App.Conversation, dir)
|
||||
} else if m.selectedMessage > 0 {
|
||||
cmd = m.cycleSelectedReply(&m.App.Messages[m.selectedMessage-1], dir)
|
||||
}
|
||||
@@ -162,7 +162,6 @@ func (m *Model) handleInputKey(msg tea.KeyMsg) tea.Cmd {
|
||||
m.input.Blur()
|
||||
return shared.KeyHandled(msg)
|
||||
case "ctrl+s":
|
||||
// TODO: call a "handleSend" function which returns a tea.Cmd
|
||||
if m.state != idle {
|
||||
return nil
|
||||
}
|
||||
@@ -172,7 +171,7 @@ func (m *Model) handleInputKey(msg tea.KeyMsg) tea.Cmd {
|
||||
return shared.KeyHandled(msg)
|
||||
}
|
||||
|
||||
if len(m.App.Messages) > 0 && m.App.Messages[len(m.App.Messages)-1].Role == api.MessageRoleUser {
|
||||
if len(m.App.Messages) > 0 && m.App.Messages[len(m.App.Messages)-1].Role.IsUser() {
|
||||
return shared.WrapError(fmt.Errorf("Can't reply to a user message"))
|
||||
}
|
||||
|
||||
|
||||
@@ -75,7 +75,7 @@ func (m *Model) Update(msg tea.Msg) (shared.ViewModel, tea.Cmd) {
|
||||
m.rebuildMessageCache()
|
||||
m.updateContent()
|
||||
|
||||
if m.App.Conversation != nil && m.App.Conversation.ID > 0 {
|
||||
if m.App.Conversation.ID > 0 {
|
||||
// (re)load conversation contents
|
||||
cmds = append(cmds, m.loadConversationMessages())
|
||||
}
|
||||
@@ -133,7 +133,7 @@ func (m *Model) Update(msg tea.Msg) (shared.ViewModel, tea.Cmd) {
|
||||
case msgChatResponse:
|
||||
m.state = idle
|
||||
|
||||
reply := (*conversation.Message)(msg)
|
||||
reply := conversation.Message(msg)
|
||||
reply.Content = strings.TrimSpace(reply.Content)
|
||||
|
||||
last := len(m.App.Messages) - 1
|
||||
@@ -142,16 +142,15 @@ func (m *Model) Update(msg tea.Msg) (shared.ViewModel, tea.Cmd) {
|
||||
}
|
||||
|
||||
if m.App.Messages[last].Role.IsAssistant() {
|
||||
// TODO: handle continuations gracefully - some models support them well, others fail horribly.
|
||||
m.setMessage(last, *reply)
|
||||
// TODO: handle continuations gracefully - only some models support them
|
||||
m.setMessage(last, reply)
|
||||
} else {
|
||||
m.addMessage(*reply)
|
||||
m.addMessage(reply)
|
||||
}
|
||||
|
||||
switch reply.Role {
|
||||
case api.MessageRoleToolCall:
|
||||
if reply.Role == api.MessageRoleToolCall {
|
||||
// TODO: user confirmation before execution
|
||||
// m.state = waitingForConfirmation
|
||||
// m.state = confirmToolUse
|
||||
cmds = append(cmds, m.executeToolCalls(reply.ToolCalls))
|
||||
}
|
||||
|
||||
@@ -159,11 +158,9 @@ func (m *Model) Update(msg tea.Msg) (shared.ViewModel, tea.Cmd) {
|
||||
cmds = append(cmds, m.persistConversation())
|
||||
}
|
||||
|
||||
if m.App.Conversation.Title == "" {
|
||||
if m.App.Conversation.Title == "" && len(m.App.Messages) > 0 {
|
||||
cmds = append(cmds, m.generateConversationTitle())
|
||||
}
|
||||
|
||||
m.updateContent()
|
||||
case msgChatResponseCanceled:
|
||||
m.state = idle
|
||||
m.updateContent()
|
||||
@@ -194,8 +191,8 @@ func (m *Model) Update(msg tea.Msg) (shared.ViewModel, tea.Cmd) {
|
||||
case msgConversationTitleGenerated:
|
||||
title := string(msg)
|
||||
m.App.Conversation.Title = title
|
||||
if m.persistence {
|
||||
cmds = append(cmds, m.updateConversationTitle(m.App.Conversation))
|
||||
if m.persistence && m.App.Conversation.ID > 0 {
|
||||
cmds = append(cmds, m.persistConversation())
|
||||
}
|
||||
case cursor.BlinkMsg:
|
||||
if m.state == pendingResponse {
|
||||
@@ -205,14 +202,13 @@ func (m *Model) Update(msg tea.Msg) (shared.ViewModel, tea.Cmd) {
|
||||
m.updateContent()
|
||||
}
|
||||
case msgConversationPersisted:
|
||||
m.App.Conversation = msg.conversation
|
||||
m.App.Messages = msg.messages
|
||||
m.App.Conversation = conversation.Conversation(msg)
|
||||
cmds = append(cmds, m.persistMessages())
|
||||
case msgMessagesPersisted:
|
||||
m.App.Messages = msg
|
||||
m.rebuildMessageCache()
|
||||
m.updateContent()
|
||||
case msgMessageCloned:
|
||||
if msg.Parent == nil {
|
||||
m.App.Conversation = msg.Conversation
|
||||
}
|
||||
cmds = append(cmds, m.loadConversationMessages())
|
||||
case msgSelectedRootCycled, msgSelectedReplyCycled, msgMessageUpdated:
|
||||
cmds = append(cmds, m.loadConversationMessages())
|
||||
|
||||
@@ -71,7 +71,7 @@ func (m *Model) renderMessageHeading(i int, message *conversation.Message) strin
|
||||
prefix = " "
|
||||
}
|
||||
|
||||
if i == 0 && len(m.App.Conversation.RootMessages) > 1 && m.App.Conversation.SelectedRootID != nil {
|
||||
if i == 0 && m.App.Conversation.SelectedRootID != nil && len(m.App.Conversation.RootMessages) > 1 {
|
||||
selectedRootIndex := 0
|
||||
for j, reply := range m.App.Conversation.RootMessages {
|
||||
if reply.ID == *m.App.Conversation.SelectedRootID {
|
||||
@@ -261,7 +261,7 @@ func (m *Model) Content(width, height int) string {
|
||||
func (m *Model) Header(width int) string {
|
||||
titleStyle := lipgloss.NewStyle().Bold(true)
|
||||
var title string
|
||||
if m.App.Conversation != nil && m.App.Conversation.Title != "" {
|
||||
if m.App.Conversation.Title != "" {
|
||||
title = m.App.Conversation.Title
|
||||
} else {
|
||||
title = "Untitled"
|
||||
|
||||
Reference in New Issue
Block a user