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 {
switch *m {
func (m MessageRole) IsAssistant() bool {
switch m {
case MessageRoleAssistant, MessageRoleToolCall:
return true
}
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.
func (m MessageRole) FriendlyRole() string {
switch m {

View File

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

View File

@ -12,17 +12,6 @@ import (
)
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 {
case focusInput:
cmd := m.handleInputKey(msg)
@ -50,11 +39,19 @@ func (m *Model) handleInput(msg tea.KeyMsg) tea.Cmd {
m.stopSignal <- struct{}{}
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":
m.persistence = !m.persistence
return shared.KeyHandled(msg)
case "ctrl+t":
m.showToolResults = !m.showToolResults
m.showDetails = !m.showDetails
m.rebuildMessageCache()
m.updateContent()
return shared.KeyHandled(msg)
@ -72,6 +69,27 @@ func (m *Model) handleInput(msg tea.KeyMsg) tea.Cmd {
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
func (m *Model) handleMessagesKey(msg tea.KeyMsg) tea.Cmd {
switch msg.String() {
@ -90,25 +108,19 @@ func (m *Model) handleMessagesKey(msg tea.KeyMsg) tea.Cmd {
)
}
return nil
case "ctrl+k":
if m.selectedMessage > 0 && len(m.App.Messages) == len(m.messageOffsets) {
m.selectedMessage--
m.updateContent()
offset := m.messageOffsets[m.selectedMessage]
tuiutil.ScrollIntoView(&m.content, offset, m.content.Height/2)
case "ctrl+k", "ctrl+up":
if m.selectedMessage > 0 {
m.scrollSelection(-1)
}
return shared.KeyHandled(msg)
case "ctrl+j":
if m.selectedMessage < len(m.App.Messages)-1 && len(m.App.Messages) == len(m.messageOffsets) {
m.selectedMessage++
m.updateContent()
offset := m.messageOffsets[m.selectedMessage]
tuiutil.ScrollIntoView(&m.content, offset, m.content.Height/2)
case "ctrl+j", "ctrl+down":
if m.selectedMessage < len(m.App.Messages)-1 {
m.scrollSelection(1)
}
return shared.KeyHandled(msg)
case "ctrl+h", "ctrl+l":
case "ctrl+h", "ctrl+left", "ctrl+l", "ctrl+right":
dir := model.CyclePrev
if msg.String() == "ctrl+l" {
if msg.String() == "ctrl+l" || msg.String() == "ctrl+right" {
dir = model.CycleNext
}
@ -120,7 +132,7 @@ func (m *Model) handleMessagesKey(msg tea.KeyMsg) tea.Cmd {
}
return cmd
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) {
m.App.Messages = m.App.Messages[: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 nil
return nil
}
// handleInputKey handles input when the input textarea is focused

View File

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