From 24b5cdbbf6540d48e2357c3dd2cd0787bf8a7f2c Mon Sep 17 00:00:00 2001 From: Matt Low Date: Mon, 16 Sep 2024 14:04:08 +0000 Subject: [PATCH] More monior TUI refactor/cleanup `tui/tui.go` is no longer responsible for passing window resize updates to all views, instead we request a new window size message to be sent at the same time we enter the view, allowing the view to catch and handle it. Add `Initialized` to `tui/shared/View` model, now we only call `Init` on a view before entering it for the first time, rather than calling `Init` on all views when the application starts. Renames file, small cleanups --- pkg/tui/model/model.go | 2 - pkg/tui/shared/shared.go | 7 +++ pkg/tui/tui.go | 30 +++++++----- .../views/chat/{conversation.go => cmds.go} | 39 ++------------- pkg/tui/views/chat/update.go | 48 ++++++++++++++----- pkg/tui/views/conversations/conversations.go | 8 +--- 6 files changed, 68 insertions(+), 66 deletions(-) rename pkg/tui/views/chat/{conversation.go => cmds.go} (76%) diff --git a/pkg/tui/model/model.go b/pkg/tui/model/model.go index 0e2bbfe..c91e518 100644 --- a/pkg/tui/model/model.go +++ b/pkg/tui/model/model.go @@ -16,7 +16,6 @@ type LoadedConversation struct { LastReply api.Message } -// AppModel represents the application data model type AppModel struct { Ctx *lmcli.Context Conversations []LoadedConversation @@ -193,7 +192,6 @@ func (a *AppModel) PromptLLM(messages []api.Message, chatReplyChunks chan api.Ch ) } -// Helper function func cycleSelectedMessage(selected *api.Message, choices []api.Message, dir MessageCycleDirection) (*api.Message, error) { currentIndex := -1 for i, reply := range choices { diff --git a/pkg/tui/shared/shared.go b/pkg/tui/shared/shared.go index 2c06841..673bdf8 100644 --- a/pkg/tui/shared/shared.go +++ b/pkg/tui/shared/shared.go @@ -5,6 +5,7 @@ import ( ) type Shared struct { + Initialized bool Width int Height int Err error @@ -29,6 +30,12 @@ type ( MsgError error ) +func ViewEnter() tea.Cmd { + return func() tea.Msg { + return MsgViewEnter{} + } +} + func WrapError(err error) tea.Cmd { return func() tea.Msg { return MsgError(err) diff --git a/pkg/tui/tui.go b/pkg/tui/tui.go index ea48b1f..09d766f 100644 --- a/pkg/tui/tui.go +++ b/pkg/tui/tui.go @@ -24,12 +24,12 @@ type LaunchOptions struct { } type Model struct { - App *model.AppModel - view shared.View + App *model.AppModel + view shared.View + + // views chat chat.Model conversations conversations.Model - Width int - Height int } func initialModel(ctx *lmcli.Context, opts LaunchOptions) Model { @@ -50,8 +50,6 @@ func initialModel(ctx *lmcli.Context, opts LaunchOptions) Model { func (m Model) Init() tea.Cmd { return tea.Batch( - m.conversations.Init(), - m.chat.Init(), func() tea.Msg { return shared.MsgViewChange(m.view) }, @@ -96,15 +94,23 @@ func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { } case shared.MsgViewChange: m.view = shared.View(msg) + + var cmds []tea.Cmd switch m.view { - case shared.StateChat: - m.chat.HandleResize(m.Width, m.Height) case shared.StateConversations: - m.conversations.HandleResize(m.Width, m.Height) + if !m.conversations.Initialized { + cmds = append(cmds, m.conversations.Init()) + m.conversations.Initialized = true + } + case shared.StateChat: + if !m.chat.Initialized { + cmds = append(cmds, m.chat.Init()) + m.chat.Initialized = true + } } - return m, func() tea.Msg { return shared.MsgViewEnter(struct{}{}) } - case tea.WindowSizeMsg: - m.Width, m.Height = msg.Width, msg.Height + cmds = append(cmds, tea.WindowSize(), shared.ViewEnter()) + + return m, tea.Batch(cmds...) } var cmd tea.Cmd diff --git a/pkg/tui/views/chat/conversation.go b/pkg/tui/views/chat/cmds.go similarity index 76% rename from pkg/tui/views/chat/conversation.go rename to pkg/tui/views/chat/cmds.go index 1ccbc1c..ae1bf74 100644 --- a/pkg/tui/views/chat/conversation.go +++ b/pkg/tui/views/chat/cmds.go @@ -9,39 +9,9 @@ import ( tea "github.com/charmbracelet/bubbletea" ) -func (m *Model) setMessage(i int, msg api.Message) { - if i >= len(m.App.Messages) { - panic("i out of range") - } - m.App.Messages[i] = msg - m.messageCache[i] = m.renderMessage(i) -} - -func (m *Model) addMessage(msg api.Message) { - m.App.Messages = append(m.App.Messages, msg) - m.messageCache = append(m.messageCache, m.renderMessage(len(m.App.Messages)-1)) -} - -func (m *Model) setMessageContents(i int, content string) { - if i >= len(m.App.Messages) { - panic("i out of range") - } - m.App.Messages[i].Content = content - m.messageCache[i] = m.renderMessage(i) -} - -func (m *Model) rebuildMessageCache() { - m.messageCache = make([]string, len(m.App.Messages)) - for i := range m.App.Messages { - m.messageCache[i] = m.renderMessage(i) - } -} - -func (m *Model) updateContent() { - atBottom := m.content.AtBottom() - m.content.SetContent(m.conversationMessagesView()) - if atBottom { - m.content.GotoBottom() +func (m *Model) waitForResponseChunk() tea.Cmd { + return func() tea.Msg { + return msgChatResponseChunk(<-m.chatReplyChunks) } } @@ -97,6 +67,7 @@ func (m *Model) updateMessageContent(message *api.Message) tea.Cmd { func (m *Model) cycleSelectedRoot(conv *api.Conversation, dir model.MessageCycleDirection) tea.Cmd { if len(m.App.RootMessages) < 2 { + return nil } @@ -153,11 +124,9 @@ 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 msgChatResponse(resp) } } diff --git a/pkg/tui/views/chat/update.go b/pkg/tui/views/chat/update.go index 26b2c2d..a8bd0ac 100644 --- a/pkg/tui/views/chat/update.go +++ b/pkg/tui/views/chat/update.go @@ -11,19 +11,39 @@ import ( tea "github.com/charmbracelet/bubbletea" ) -func (m *Model) HandleResize(width, height int) { - m.Width, m.Height = width, height - m.content.Width = width - m.input.SetWidth(width - m.input.FocusedStyle.Base.GetHorizontalFrameSize()) - if len(m.App.Messages) > 0 { - m.rebuildMessageCache() - m.updateContent() +func (m *Model) setMessage(i int, msg api.Message) { + if i >= len(m.App.Messages) { + panic("i out of range") + } + m.App.Messages[i] = msg + m.messageCache[i] = m.renderMessage(i) +} + +func (m *Model) addMessage(msg api.Message) { + m.App.Messages = append(m.App.Messages, msg) + m.messageCache = append(m.messageCache, m.renderMessage(len(m.App.Messages)-1)) +} + +func (m *Model) setMessageContents(i int, content string) { + if i >= len(m.App.Messages) { + panic("i out of range") + } + m.App.Messages[i].Content = content + m.messageCache[i] = m.renderMessage(i) +} + +func (m *Model) rebuildMessageCache() { + m.messageCache = make([]string, len(m.App.Messages)) + for i := range m.App.Messages { + m.messageCache[i] = m.renderMessage(i) } } -func (m *Model) waitForResponseChunk() tea.Cmd { - return func() tea.Msg { - return msgChatResponseChunk(<-m.chatReplyChunks) +func (m *Model) updateContent() { + atBottom := m.content.AtBottom() + m.content.SetContent(m.conversationMessagesView()) + if atBottom { + m.content.GotoBottom() } } @@ -31,7 +51,13 @@ func (m Model) Update(msg tea.Msg) (Model, tea.Cmd) { var cmds []tea.Cmd switch msg := msg.(type) { case tea.WindowSizeMsg: - m.HandleResize(msg.Width, msg.Height) + m.Width, m.Height = msg.Width, msg.Height + m.content.Width = msg.Width + m.input.SetWidth(msg.Width - m.input.FocusedStyle.Base.GetHorizontalFrameSize()) + if len(m.App.Messages) > 0 { + m.rebuildMessageCache() + m.updateContent() + } case shared.MsgViewEnter: // wake up spinners and cursors cmds = append(cmds, cursor.Blink, m.spinner.Tick) diff --git a/pkg/tui/views/conversations/conversations.go b/pkg/tui/views/conversations/conversations.go index 26642e0..f3beab1 100644 --- a/pkg/tui/views/conversations/conversations.go +++ b/pkg/tui/views/conversations/conversations.go @@ -127,11 +127,6 @@ func (m Model) Init() tea.Cmd { return m.loadConversations() } -func (m *Model) HandleResize(width, height int) { - m.Width, m.Height = width, height - m.content.Width = width -} - func (m Model) Update(msg tea.Msg) (Model, tea.Cmd) { var cmds []tea.Cmd switch msg := msg.(type) { @@ -139,7 +134,8 @@ func (m Model) Update(msg tea.Msg) (Model, tea.Cmd) { cmds = append(cmds, m.loadConversations()) m.content.SetContent(m.renderConversationList()) case tea.WindowSizeMsg: - m.HandleResize(msg.Width, msg.Height) + m.Width, m.Height = msg.Width, msg.Height + m.content.Width = msg.Width m.content.SetContent(m.renderConversationList()) case msgConversationsLoaded: m.App.Conversations = msg