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" "git.mlow.ca/mlow/lmcli/pkg/api"
cmdutil "git.mlow.ca/mlow/lmcli/pkg/cmd/util" cmdutil "git.mlow.ca/mlow/lmcli/pkg/cmd/util"
"git.mlow.ca/mlow/lmcli/pkg/lmcli" "git.mlow.ca/mlow/lmcli/pkg/lmcli"
"git.mlow.ca/mlow/lmcli/pkg/tui/shared"
) )
type LoadedConversation struct { type LoadedConversation struct {
@ -59,7 +58,7 @@ func (m *AppModel) NewConversation() {
func (m *AppModel) LoadConversations() (error, []LoadedConversation) { func (m *AppModel) LoadConversations() (error, []LoadedConversation) {
messages, err := m.Ctx.Store.LatestConversationMessages() messages, err := m.Ctx.Store.LatestConversationMessages()
if err != nil { 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)) conversations := make([]LoadedConversation, len(messages))

View File

@ -32,7 +32,7 @@ type (
// sent to a state when it is entered // sent to a state when it is entered
MsgViewEnter struct{} MsgViewEnter struct{}
// sent when a recoverable error occurs (displayed to user) // sent when a recoverable error occurs (displayed to user)
MsgError error MsgError struct { Err error }
// sent when the view has handled a key input // sent when the view has handled a key input
MsgKeyHandled tea.KeyMsg MsgKeyHandled tea.KeyMsg
) )
@ -57,6 +57,10 @@ func KeyHandled(key tea.KeyMsg) tea.Cmd {
func WrapError(err error) tea.Cmd { func WrapError(err error) tea.Cmd {
return func() tea.Msg { 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) m.activeView = shared.View(msg)
return m, tea.Batch(tea.WindowSize(), shared.ViewEnter()) return m, tea.Batch(tea.WindowSize(), shared.ViewEnter())
case shared.MsgError: 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) view, cmd := m.views[m.activeView].Update(msg)
@ -124,12 +125,12 @@ func (m *Model) View() string {
if content != "" { if content != "" {
sections = append(sections, content) sections = append(sections, content)
} }
if len(errBanners) > 0 {
sections = append(sections, lipgloss.JoinVertical(lipgloss.Left, errBanners...))
}
if footer != "" { if footer != "" {
sections = append(sections, footer) sections = append(sections, footer)
} }
for _, errBanner := range errBanners {
sections = append(sections, errBanner)
}
return lipgloss.JoinVertical(lipgloss.Left, sections...) return lipgloss.JoinVertical(lipgloss.Left, sections...)
} }

View File

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

View File

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

View File

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

View File

@ -182,7 +182,7 @@ func (m *Model) loadConversations() tea.Cmd {
return func() tea.Msg { return func() tea.Msg {
err, conversations := m.App.LoadConversations() err, conversations := m.App.LoadConversations()
if err != nil { 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) return msgConversationsLoaded(conversations)
} }
@ -192,7 +192,7 @@ func (m *Model) deleteConversation(conv api.Conversation) tea.Cmd {
return func() tea.Msg { return func() tea.Msg {
err := m.App.Ctx.Store.DeleteConversation(&conv) err := m.App.Ctx.Store.DeleteConversation(&conv)
if err != nil { 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{} return msgConversationDeleted{}
} }