tui: Error handling tweak

Moved errors to bottom of screen, fix infinite loop by typing errors
properly
This commit is contained in:
Matt Low 2024-09-23 03:37:01 +00:00
parent 172bfc57e1
commit 3ec2675632
7 changed files with 29 additions and 23 deletions

View File

@ -8,7 +8,6 @@ import (
"git.mlow.ca/mlow/lmcli/pkg/api"
cmdutil "git.mlow.ca/mlow/lmcli/pkg/cmd/util"
"git.mlow.ca/mlow/lmcli/pkg/lmcli"
"git.mlow.ca/mlow/lmcli/pkg/tui/shared"
)
type LoadedConversation struct {
@ -59,7 +58,7 @@ func (m *AppModel) NewConversation() {
func (m *AppModel) LoadConversations() (error, []LoadedConversation) {
messages, err := m.Ctx.Store.LatestConversationMessages()
if err != nil {
return shared.MsgError(fmt.Errorf("Could not load conversations: %v", err)), nil
return fmt.Errorf("Could not load conversations: %v", err), nil
}
conversations := make([]LoadedConversation, len(messages))

View File

@ -32,7 +32,7 @@ type (
// sent to a state when it is entered
MsgViewEnter struct{}
// sent when a recoverable error occurs (displayed to user)
MsgError error
MsgError struct { Err error }
// sent when the view has handled a key input
MsgKeyHandled tea.KeyMsg
)
@ -57,6 +57,10 @@ func KeyHandled(key tea.KeyMsg) tea.Cmd {
func WrapError(err error) tea.Cmd {
return func() tea.Msg {
return MsgError(err)
return MsgError{ Err: err }
}
}
func AsMsgError(err error) MsgError {
return MsgError{ Err: err }
}

View File

@ -91,7 +91,8 @@ func (m *Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
m.activeView = shared.View(msg)
return m, tea.Batch(tea.WindowSize(), shared.ViewEnter())
case shared.MsgError:
m.errs = append(m.errs, msg)
m.errs = append(m.errs, msg.Err)
return m, nil
}
view, cmd := m.views[m.activeView].Update(msg)
@ -124,12 +125,12 @@ func (m *Model) View() string {
if content != "" {
sections = append(sections, content)
}
if len(errBanners) > 0 {
sections = append(sections, lipgloss.JoinVertical(lipgloss.Left, errBanners...))
}
if footer != "" {
sections = append(sections, footer)
}
for _, errBanner := range errBanners {
sections = append(sections, errBanner)
}
return lipgloss.JoinVertical(lipgloss.Left, sections...)
}

View File

@ -25,11 +25,13 @@ type (
}
// sent when a conversation's messages are laoded
msgConversationMessagesLoaded struct {
messages []api.Message
messages []api.Message
rootMessages []api.Message
}
// a special case of common.MsgError that stops the response waiting animation
msgChatResponseError error
msgChatResponseError struct {
Err error
}
// sent on each chunk received from LLM
msgChatResponseChunk api.Chunk
// sent on each completed reply
@ -72,9 +74,9 @@ const (
type Model struct {
// App state
App *model.AppModel
App *model.AppModel
Height int
Width int
Width int
// Chat view state
state state // current overall status of the view
@ -108,8 +110,8 @@ func Chat(app *model.AppModel) *Model {
m := Model{
App: app,
state: idle,
persistence: true,
state: idle,
persistence: true,
stopSignal: make(chan struct{}),
replyChan: make(chan api.Message),

View File

@ -19,11 +19,11 @@ func (m *Model) loadConversationMessages() tea.Cmd {
return func() tea.Msg {
messages, err := m.App.LoadConversationMessages()
if err != nil {
return shared.MsgError(err)
return shared.AsMsgError(err)
}
rootMessages, err := m.App.LoadConversationRootMessages()
if err != nil {
return shared.MsgError(err)
return shared.AsMsgError(err)
}
return msgConversationMessagesLoaded{
messages, rootMessages,
@ -35,7 +35,7 @@ func (m *Model) generateConversationTitle() tea.Cmd {
return func() tea.Msg {
title, err := m.App.GenerateConversationTitle(m.App.Messages)
if err != nil {
return shared.MsgError(err)
return shared.AsMsgError(err)
}
return msgConversationTitleGenerated(title)
}
@ -104,7 +104,7 @@ func (m *Model) persistConversation() tea.Cmd {
return func() tea.Msg {
conversation, messages, err := m.App.PersistConversation(m.App.Conversation, m.App.Messages)
if err != nil {
return shared.MsgError(err)
return shared.AsMsgError(err)
}
return msgConversationPersisted{conversation.ID == 0, conversation, messages}
}
@ -114,7 +114,7 @@ func (m *Model) executeToolCalls(toolCalls []api.ToolCall) tea.Cmd {
return func() tea.Msg {
results, err := m.App.ExecuteToolCalls(toolCalls)
if err != nil {
return shared.MsgError(err)
return shared.AsMsgError(err)
}
return msgToolResults(results)
}
@ -131,7 +131,7 @@ func (m *Model) promptLLM() tea.Cmd {
return func() tea.Msg {
resp, err := m.App.PromptLLM(m.App.Messages, m.chatReplyChunks, m.stopSignal)
if err != nil {
return msgChatResponseError(err)
return msgChatResponseError{ Err: err }
}
return msgChatResponse(resp)
}

View File

@ -170,7 +170,7 @@ func (m *Model) Update(msg tea.Msg) (shared.ViewModel, tea.Cmd) {
case msgChatResponseError:
m.state = idle
m.updateContent()
return m, shared.WrapError(msg)
return m, shared.WrapError(msg.Err)
case msgToolResults:
last := len(m.App.Messages) - 1
if last < 0 {

View File

@ -182,7 +182,7 @@ func (m *Model) loadConversations() tea.Cmd {
return func() tea.Msg {
err, conversations := m.App.LoadConversations()
if err != nil {
return shared.MsgError(fmt.Errorf("Could not load conversations: %v", err))
return shared.AsMsgError(fmt.Errorf("Could not load conversations: %v", err))
}
return msgConversationsLoaded(conversations)
}
@ -192,7 +192,7 @@ func (m *Model) deleteConversation(conv api.Conversation) tea.Cmd {
return func() tea.Msg {
err := m.App.Ctx.Store.DeleteConversation(&conv)
if err != nil {
return shared.MsgError(fmt.Errorf("Could not delete conversation: %v", err))
return shared.AsMsgError(fmt.Errorf("Could not delete conversation: %v", err))
}
return msgConversationDeleted{}
}