Display generation model in message header and other tweaks

Adjusted `ctrl+t` in chat view to toggle `showDetails` which toggles the
display of system messages, message metadata (generation model), and
tool call details

Modified message selection update logic to skip messages that aren't
shown
This commit is contained in:
Matt Low 2024-09-30 20:21:51 +00:00
parent 5d13c3e056
commit bb48bc9abd
4 changed files with 93 additions and 59 deletions

View File

@ -71,14 +71,22 @@ func ApplySystemPrompt(m []Message, system string, force bool) []Message {
} }
} }
func (m *MessageRole) IsAssistant() bool { func (m MessageRole) IsAssistant() bool {
switch *m { switch m {
case MessageRoleAssistant, MessageRoleToolCall: case MessageRoleAssistant, MessageRoleToolCall:
return true return true
} }
return false return false
} }
func (m MessageRole) IsSystem() bool {
switch m {
case MessageRoleSystem:
return true
}
return false
}
// FriendlyRole returns a human friendly signifier for the message's role. // FriendlyRole returns a human friendly signifier for the message's role.
func (m MessageRole) FriendlyRole() string { func (m MessageRole) FriendlyRole() string {
switch m { switch m {

View File

@ -89,11 +89,11 @@ type Model struct {
persistence bool // whether we will save new messages in the conversation persistence bool // whether we will save new messages in the conversation
// UI state // UI state
focus focusState focus focusState
wrap bool // whether message content is wrapped to viewport width showDetails bool // whether various details are shown in the UI (e.g. system prompt, tool calls/results, message metadata)
showToolResults bool // whether tool calls and results are shown wrap bool // whether message content is wrapped to viewport width
messageCache []string // cache of syntax highlighted and wrapped message content messageCache []string // cache of syntax highlighted and wrapped message content
messageOffsets []int messageOffsets []int
// ui elements // ui elements
content viewport.Model content viewport.Model

View File

@ -12,17 +12,6 @@ import (
) )
func (m *Model) handleInput(msg tea.KeyMsg) tea.Cmd { func (m *Model) handleInput(msg tea.KeyMsg) tea.Cmd {
switch msg.String() {
case "ctrl+g":
if m.state == pendingResponse {
m.stopSignal <- struct{}{}
return shared.KeyHandled(msg)
}
return func() tea.Msg {
return shared.MsgViewChange(shared.ViewSettings)
}
}
switch m.focus { switch m.focus {
case focusInput: case focusInput:
cmd := m.handleInputKey(msg) cmd := m.handleInputKey(msg)
@ -50,11 +39,19 @@ func (m *Model) handleInput(msg tea.KeyMsg) tea.Cmd {
m.stopSignal <- struct{}{} m.stopSignal <- struct{}{}
return shared.KeyHandled(msg) return shared.KeyHandled(msg)
} }
case "ctrl+g":
if m.state == pendingResponse {
m.stopSignal <- struct{}{}
return shared.KeyHandled(msg)
}
return func() tea.Msg {
return shared.MsgViewChange(shared.ViewSettings)
}
case "ctrl+p": case "ctrl+p":
m.persistence = !m.persistence m.persistence = !m.persistence
return shared.KeyHandled(msg) return shared.KeyHandled(msg)
case "ctrl+t": case "ctrl+t":
m.showToolResults = !m.showToolResults m.showDetails = !m.showDetails
m.rebuildMessageCache() m.rebuildMessageCache()
m.updateContent() m.updateContent()
return shared.KeyHandled(msg) return shared.KeyHandled(msg)
@ -72,6 +69,27 @@ func (m *Model) handleInput(msg tea.KeyMsg) tea.Cmd {
return nil return nil
} }
func (m *Model) scrollSelection(dir int) {
if m.selectedMessage + dir < 0 || m.selectedMessage + dir >= len(m.App.Messages) {
return
}
newIdx := m.selectedMessage
for i := newIdx + dir; i >= 0 && i < len(m.App.Messages); i += dir{
if !m.showDetails && m.App.Messages[i].Role.IsSystem() {
continue
}
newIdx = i
break
}
if newIdx != m.selectedMessage {
m.selectedMessage = newIdx
m.updateContent()
yOffset := m.messageOffsets[m.selectedMessage]
tuiutil.ScrollIntoView(&m.content, yOffset, m.content.Height/2)
}
}
// handleMessagesKey handles input when the messages pane is focused // handleMessagesKey handles input when the messages pane is focused
func (m *Model) handleMessagesKey(msg tea.KeyMsg) tea.Cmd { func (m *Model) handleMessagesKey(msg tea.KeyMsg) tea.Cmd {
switch msg.String() { switch msg.String() {
@ -90,25 +108,19 @@ func (m *Model) handleMessagesKey(msg tea.KeyMsg) tea.Cmd {
) )
} }
return nil return nil
case "ctrl+k": case "ctrl+k", "ctrl+up":
if m.selectedMessage > 0 && len(m.App.Messages) == len(m.messageOffsets) { if m.selectedMessage > 0 {
m.selectedMessage-- m.scrollSelection(-1)
m.updateContent()
offset := m.messageOffsets[m.selectedMessage]
tuiutil.ScrollIntoView(&m.content, offset, m.content.Height/2)
} }
return shared.KeyHandled(msg) return shared.KeyHandled(msg)
case "ctrl+j": case "ctrl+j", "ctrl+down":
if m.selectedMessage < len(m.App.Messages)-1 && len(m.App.Messages) == len(m.messageOffsets) { if m.selectedMessage < len(m.App.Messages)-1 {
m.selectedMessage++ m.scrollSelection(1)
m.updateContent()
offset := m.messageOffsets[m.selectedMessage]
tuiutil.ScrollIntoView(&m.content, offset, m.content.Height/2)
} }
return shared.KeyHandled(msg) return shared.KeyHandled(msg)
case "ctrl+h", "ctrl+l": case "ctrl+h", "ctrl+left", "ctrl+l", "ctrl+right":
dir := model.CyclePrev dir := model.CyclePrev
if msg.String() == "ctrl+l" { if msg.String() == "ctrl+l" || msg.String() == "ctrl+right" {
dir = model.CycleNext dir = model.CycleNext
} }
@ -120,7 +132,7 @@ func (m *Model) handleMessagesKey(msg tea.KeyMsg) tea.Cmd {
} }
return cmd return cmd
case "ctrl+r": case "ctrl+r":
// resubmit the conversation with all messages up until and including the selected message // prompt the model with all messages up to and including the selected message
if m.state == idle && m.selectedMessage < len(m.App.Messages) { if m.state == idle && m.selectedMessage < len(m.App.Messages) {
m.App.Messages = m.App.Messages[:m.selectedMessage+1] m.App.Messages = m.App.Messages[:m.selectedMessage+1]
m.messageCache = m.messageCache[:m.selectedMessage+1] m.messageCache = m.messageCache[:m.selectedMessage+1]
@ -130,7 +142,7 @@ func (m *Model) handleMessagesKey(msg tea.KeyMsg) tea.Cmd {
return cmd return cmd
} }
} }
return nil return nil
} }
// handleInputKey handles input when the input textarea is focused // handleInputKey handles input when the input textarea is focused

View File

@ -16,14 +16,19 @@ import (
// styles // styles
var ( var (
boldStyle = lipgloss.NewStyle().Bold(true)
faintStyle = lipgloss.NewStyle().Faint(true)
boldFaintStyle = lipgloss.NewStyle().Faint(true).Bold(true)
messageHeadingStyle = lipgloss.NewStyle(). messageHeadingStyle = lipgloss.NewStyle().
MarginTop(1). MarginTop(1).
MarginBottom(1). MarginBottom(1)
Bold(true)
userStyle = lipgloss.NewStyle().Faint(true).Foreground(lipgloss.Color("10")) userStyle = boldFaintStyle.Foreground(lipgloss.Color("10"))
assistantStyle = lipgloss.NewStyle().Faint(true).Foreground(lipgloss.Color("12")) assistantStyle = boldFaintStyle.Foreground(lipgloss.Color("12"))
systemStyle = boldStyle.Foreground(lipgloss.Color("8"))
messageStyle = lipgloss.NewStyle(). messageStyle = lipgloss.NewStyle().
PaddingLeft(2). PaddingLeft(2).
@ -40,13 +45,10 @@ var (
) )
func (m *Model) renderMessageHeading(i int, message *api.Message) string { func (m *Model) renderMessageHeading(i int, message *api.Message) string {
icon := ""
friendly := message.Role.FriendlyRole() friendly := message.Role.FriendlyRole()
style := lipgloss.NewStyle().Faint(true).Bold(true) style := systemStyle
switch message.Role { switch message.Role {
case api.MessageRoleSystem:
icon = "⚙️"
case api.MessageRoleUser: case api.MessageRoleUser:
style = userStyle style = userStyle
case api.MessageRoleAssistant: case api.MessageRoleAssistant:
@ -54,16 +56,19 @@ func (m *Model) renderMessageHeading(i int, message *api.Message) string {
case api.MessageRoleToolCall: case api.MessageRoleToolCall:
style = assistantStyle style = assistantStyle
friendly = api.MessageRoleAssistant.FriendlyRole() friendly = api.MessageRoleAssistant.FriendlyRole()
case api.MessageRoleSystem:
case api.MessageRoleToolResult: case api.MessageRoleToolResult:
icon = "🔧"
} }
user := style.Render(icon + friendly) user := style.Render(friendly)
var prefix string var prefix, suffix string
var suffix string
faint := lipgloss.NewStyle().Faint(true) if i == m.selectedMessage {
prefix = "> "
} else {
prefix = " "
}
if i == 0 && len(m.App.RootMessages) > 1 && m.App.Conversation.SelectedRootID != nil { if i == 0 && len(m.App.RootMessages) > 1 && m.App.Conversation.SelectedRootID != nil {
selectedRootIndex := 0 selectedRootIndex := 0
@ -73,7 +78,7 @@ func (m *Model) renderMessageHeading(i int, message *api.Message) string {
break break
} }
} }
suffix += faint.Render(fmt.Sprintf(" <%d/%d>", selectedRootIndex+1, len(m.App.RootMessages))) suffix += faintStyle.Render(fmt.Sprintf(" <%d/%d>", selectedRootIndex+1, len(m.App.RootMessages)))
} }
if i > 0 && len(m.App.Messages[i-1].Replies) > 1 { if i > 0 && len(m.App.Messages[i-1].Replies) > 1 {
// Find the selected reply index // Find the selected reply index
@ -84,20 +89,22 @@ func (m *Model) renderMessageHeading(i int, message *api.Message) string {
break break
} }
} }
suffix += faint.Render(fmt.Sprintf(" <%d/%d>", selectedReplyIndex+1, len(m.App.Messages[i-1].Replies))) suffix += faintStyle.Render(fmt.Sprintf(" <%d/%d>", selectedReplyIndex+1, len(m.App.Messages[i-1].Replies)))
}
if i == m.selectedMessage {
prefix = "> "
} else {
prefix = " "
} }
if message.ID == 0 { if message.ID == 0 {
suffix += faint.Render(" (not saved)") suffix += faintStyle.Render(" (not saved)")
} }
return messageHeadingStyle.Render(prefix + user + suffix) heading := prefix + user + suffix
if message.Metadata.GenerationModel != nil && m.showDetails {
heading += faintStyle.Render(
fmt.Sprintf(" | %s", *message.Metadata.GenerationModel),
)
}
return messageHeadingStyle.Render(heading)
} }
// renderMessages renders the message at the given index as it should be shown // renderMessages renders the message at the given index as it should be shown
@ -143,7 +150,7 @@ func (m *Model) renderMessage(i int) string {
var toolResults []renderedResult var toolResults []renderedResult
for _, result := range msg.ToolResults { for _, result := range msg.ToolResults {
if m.showToolResults { if m.showDetails {
var jsonResult interface{} var jsonResult interface{}
err := json.Unmarshal([]byte(result.Result), &jsonResult) err := json.Unmarshal([]byte(result.Result), &jsonResult)
if err != nil { if err != nil {
@ -206,6 +213,10 @@ func (m *Model) conversationMessagesView() string {
for i, message := range m.App.Messages { for i, message := range m.App.Messages {
m.messageOffsets[i] = lineCnt m.messageOffsets[i] = lineCnt
if !m.showDetails && message.Role.IsSystem() {
continue
}
heading := m.renderMessageHeading(i, &message) heading := m.renderMessageHeading(i, &message)
sb.WriteString(heading) sb.WriteString(heading)
sb.WriteString("\n") sb.WriteString("\n")
@ -221,6 +232,9 @@ func (m *Model) conversationMessagesView() string {
if m.state == pendingResponse && m.App.Messages[len(m.App.Messages)-1].Role != api.MessageRoleAssistant { if m.state == pendingResponse && m.App.Messages[len(m.App.Messages)-1].Role != api.MessageRoleAssistant {
heading := m.renderMessageHeading(-1, &api.Message{ heading := m.renderMessageHeading(-1, &api.Message{
Role: api.MessageRoleAssistant, Role: api.MessageRoleAssistant,
Metadata: api.MessageMeta {
GenerationModel: &m.App.Model,
},
}) })
sb.WriteString(heading) sb.WriteString(heading)
sb.WriteString("\n") sb.WriteString("\n")