tui: cache highlighted messages

Syntax highlighting is fairly expensive, and this means we no longer
need to do syntax highlighting on the entire conversaion each time a new
message chunk is received
This commit is contained in:
Matt Low 2024-03-13 16:29:06 +00:00
parent 7e002e5214
commit 94508b1dbf
1 changed files with 58 additions and 12 deletions

View File

@ -43,6 +43,7 @@ type model struct {
// ui state // ui state
focus focusState focus focusState
status string // a general status message status string // a general status message
highlightCache []string // a cache of syntax highlighted message content
// ui elements // ui elements
content viewport.Model content viewport.Model
@ -132,15 +133,15 @@ func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
m.conversation = (*models.Conversation)(msg) m.conversation = (*models.Conversation)(msg)
cmd = m.loadMessages(m.conversation) cmd = m.loadMessages(m.conversation)
case msgMessagesLoaded: case msgMessagesLoaded:
m.messages = []models.Message(msg) m.setMessages(msg)
m.updateContent() m.updateContent()
case msgResponseChunk: case msgResponseChunk:
chunk := string(msg) chunk := string(msg)
last := len(m.messages) - 1 last := len(m.messages) - 1
if last >= 0 && m.messages[last].Role == models.MessageRoleAssistant { if last >= 0 && m.messages[last].Role == models.MessageRoleAssistant {
m.messages[last].Content += chunk m.setMessageContents(last, m.messages[last].Content+chunk)
} else { } else {
m.messages = append(m.messages, models.Message{ m.addMessage(models.Message{
Role: models.MessageRoleAssistant, Role: models.MessageRoleAssistant,
Content: chunk, Content: chunk,
}) })
@ -155,9 +156,9 @@ func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
panic("Unexpected messages length handling msgReply") panic("Unexpected messages length handling msgReply")
} }
if reply.Role == models.MessageRoleToolCall && m.messages[last].Role == models.MessageRoleAssistant { if reply.Role == models.MessageRoleToolCall && m.messages[last].Role == models.MessageRoleAssistant {
m.messages[last] = reply m.setMessage(last, reply)
} else if reply.Role != models.MessageRoleAssistant { } else if reply.Role != models.MessageRoleAssistant {
m.messages = append(m.messages, reply) m.addMessage(reply)
} }
m.updateContent() m.updateContent()
cmd = m.waitForReply() cmd = m.waitForReply()
@ -239,7 +240,7 @@ func (m *model) handleInputKey(msg tea.KeyMsg) tea.Cmd {
return nil return nil
} }
m.input.SetValue("") m.input.SetValue("")
m.messages = append(m.messages, models.Message{ m.addMessage(models.Message{
Role: models.MessageRoleUser, Role: models.MessageRoleUser,
Content: userInput, Content: userInput,
}) })
@ -322,6 +323,40 @@ func (m *model) promptLLM() tea.Cmd {
} }
} }
func (m *model) setMessages(messages []models.Message) {
m.messages = messages
m.highlightCache = make([]string, len(messages))
for i, msg := range m.messages {
highlighted, _ := m.ctx.Chroma.HighlightS(msg.Content)
m.highlightCache[i] = highlighted
}
}
func (m *model) setMessage(i int, msg models.Message) {
if i >= len(m.messages) {
panic("i out of range")
}
highlighted, _ := m.ctx.Chroma.HighlightS(msg.Content)
m.messages[i] = msg
m.highlightCache[i] = highlighted
}
func (m *model) addMessage(msg models.Message) {
highlighted, _ := m.ctx.Chroma.HighlightS(msg.Content)
m.messages = append(m.messages, msg)
m.highlightCache = append(m.highlightCache, highlighted)
}
func (m *model) setMessageContents(i int, content string) {
if i >= len(m.messages) {
panic("i out of range")
}
highlighted, _ := m.ctx.Chroma.HighlightS(content)
m.messages[i].Content = content
m.highlightCache[i] = highlighted
}
// render the conversation into the main viewport
func (m *model) updateContent() { func (m *model) updateContent() {
sb := strings.Builder{} sb := strings.Builder{}
msgCnt := len(m.messages) msgCnt := len(m.messages)
@ -342,10 +377,22 @@ func (m *model) updateContent() {
icon = "🔧" icon = "🔧"
} }
sb.WriteString(fmt.Sprintf("%s\n\n", style.Render(icon + friendly))) // write message heading with space for content
header := fmt.Sprintf("%s\n\n", style.Render(icon+friendly))
sb.WriteString(header)
// TODO: render something for tool calls/results?
// write message contents
var highlighted string
if m.highlightCache[i] == "" {
highlighted = message.Content
} else {
highlighted = m.highlightCache[i]
}
contents := messageStyle.Width(m.content.Width - 5).Render(highlighted)
sb.WriteString(contents)
highlighted, _ := m.ctx.Chroma.HighlightS(message.Content)
sb.WriteString(messageStyle.Width(m.content.Width - 5).Render(highlighted))
if i < msgCnt-1 { if i < msgCnt-1 {
sb.WriteString("\n\n") sb.WriteString("\n\n")
} }
@ -389,7 +436,6 @@ func (m *model) footerView() string {
} }
footer := lipgloss.JoinHorizontal(lipgloss.Center, left, padding, right) footer := lipgloss.JoinHorizontal(lipgloss.Center, left, padding, right)
return footerStyle.Width(m.content.Width).Render(footer) return footerStyle.Width(m.content.Width).Render(footer)
} }