tui: add ability to select a message
This commit is contained in:
parent
4fb059c850
commit
3d8d3b61b3
@ -47,13 +47,15 @@ type model struct {
|
|||||||
stopSignal chan interface{}
|
stopSignal chan interface{}
|
||||||
replyChan chan models.Message
|
replyChan chan models.Message
|
||||||
replyChunkChan chan string
|
replyChunkChan chan string
|
||||||
err error
|
|
||||||
persistence bool // whether we will save new messages in the conversation
|
persistence bool // whether we will save new messages in the conversation
|
||||||
|
err error
|
||||||
|
|
||||||
// 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
|
highlightCache []string // a cache of syntax highlighted message content
|
||||||
|
messageOffsets []int
|
||||||
|
selectedMessage int
|
||||||
|
|
||||||
// ui elements
|
// ui elements
|
||||||
content viewport.Model
|
content viewport.Model
|
||||||
@ -94,8 +96,8 @@ var (
|
|||||||
headerStyle = lipgloss.NewStyle().
|
headerStyle = lipgloss.NewStyle().
|
||||||
Background(lipgloss.Color("0"))
|
Background(lipgloss.Color("0"))
|
||||||
conversationStyle = lipgloss.NewStyle().
|
conversationStyle = lipgloss.NewStyle().
|
||||||
MarginTop(1).
|
MarginTop(1).
|
||||||
MarginBottom(1)
|
MarginBottom(1)
|
||||||
footerStyle = lipgloss.NewStyle().
|
footerStyle = lipgloss.NewStyle().
|
||||||
BorderTop(true).
|
BorderTop(true).
|
||||||
BorderStyle(lipgloss.NormalBorder())
|
BorderStyle(lipgloss.NormalBorder())
|
||||||
@ -377,6 +379,8 @@ func initialModel(ctx *lmcli.Context, convShortname string) model {
|
|||||||
stopSignal: make(chan interface{}),
|
stopSignal: make(chan interface{}),
|
||||||
replyChan: make(chan models.Message),
|
replyChan: make(chan models.Message),
|
||||||
replyChunkChan: make(chan string),
|
replyChunkChan: make(chan string),
|
||||||
|
|
||||||
|
selectedMessage: -1,
|
||||||
}
|
}
|
||||||
|
|
||||||
m.content = viewport.New(0, 0)
|
m.content = viewport.New(0, 0)
|
||||||
@ -409,11 +413,44 @@ func initialModel(ctx *lmcli.Context, convShortname string) model {
|
|||||||
return m
|
return m
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// fraction is the fraction of the total screen height into view the offset
|
||||||
|
// should be scrolled into view. 0.5 = items will be snapped to middle of
|
||||||
|
// view
|
||||||
|
func scrollIntoView(vp *viewport.Model, offset int, fraction float32) {
|
||||||
|
currentOffset := vp.YOffset
|
||||||
|
if offset >= currentOffset && offset < currentOffset+vp.Height {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
distance := currentOffset - offset
|
||||||
|
if distance < 0 {
|
||||||
|
// we should scroll down until it just comes into view
|
||||||
|
vp.SetYOffset(currentOffset - (distance + (vp.Height - int(float32(vp.Height)*fraction))) + 1)
|
||||||
|
} else {
|
||||||
|
// we should scroll up
|
||||||
|
vp.SetYOffset(currentOffset - distance - int(float32(vp.Height)*fraction))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func (m *model) handleMessagesKey(msg tea.KeyMsg) tea.Cmd {
|
func (m *model) handleMessagesKey(msg tea.KeyMsg) tea.Cmd {
|
||||||
switch msg.String() {
|
switch msg.String() {
|
||||||
case "tab":
|
case "tab":
|
||||||
m.focus = focusInput
|
m.focus = focusInput
|
||||||
|
m.updateContent()
|
||||||
m.input.Focus()
|
m.input.Focus()
|
||||||
|
case "ctrl+k":
|
||||||
|
if m.selectedMessage > 0 && len(m.messages) == len(m.messageOffsets) {
|
||||||
|
m.selectedMessage--
|
||||||
|
m.updateContent()
|
||||||
|
offset := m.messageOffsets[m.selectedMessage]
|
||||||
|
scrollIntoView(&m.content, offset, 0.1)
|
||||||
|
}
|
||||||
|
case "ctrl+j":
|
||||||
|
if m.selectedMessage < len(m.messages)-1 && len(m.messages) == len(m.messageOffsets) {
|
||||||
|
m.selectedMessage++
|
||||||
|
m.updateContent()
|
||||||
|
offset := m.messageOffsets[m.selectedMessage]
|
||||||
|
scrollIntoView(&m.content, offset, 0.1)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
@ -422,6 +459,10 @@ func (m *model) handleInputKey(msg tea.KeyMsg) tea.Cmd {
|
|||||||
switch msg.String() {
|
switch msg.String() {
|
||||||
case "esc":
|
case "esc":
|
||||||
m.focus = focusMessages
|
m.focus = focusMessages
|
||||||
|
if m.selectedMessage < 0 || m.selectedMessage >= len(m.messages) {
|
||||||
|
m.selectedMessage = len(m.messages) - 1
|
||||||
|
}
|
||||||
|
m.updateContent()
|
||||||
m.input.Blur()
|
m.input.Blur()
|
||||||
case "ctrl+s":
|
case "ctrl+s":
|
||||||
userInput := strings.TrimSpace(m.input.Value())
|
userInput := strings.TrimSpace(m.input.Value())
|
||||||
@ -634,7 +675,12 @@ func (m *model) updateContent() {
|
|||||||
func (m *model) conversationView() string {
|
func (m *model) conversationView() string {
|
||||||
sb := strings.Builder{}
|
sb := strings.Builder{}
|
||||||
msgCnt := len(m.messages)
|
msgCnt := len(m.messages)
|
||||||
|
|
||||||
|
m.messageOffsets = make([]int, len(m.messages))
|
||||||
|
lineCnt := conversationStyle.GetMarginTop()
|
||||||
for i, message := range m.messages {
|
for i, message := range m.messages {
|
||||||
|
m.messageOffsets[i] = lineCnt
|
||||||
|
|
||||||
icon := "⚙️"
|
icon := "⚙️"
|
||||||
friendly := message.Role.FriendlyRole()
|
friendly := message.Role.FriendlyRole()
|
||||||
style := lipgloss.NewStyle().Bold(true).Faint(true)
|
style := lipgloss.NewStyle().Bold(true).Faint(true)
|
||||||
@ -650,18 +696,27 @@ func (m *model) conversationView() string {
|
|||||||
icon = "🔧"
|
icon = "🔧"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// write message heading with space for content
|
||||||
|
user := style.Render(icon + friendly)
|
||||||
|
|
||||||
var saved string
|
var saved string
|
||||||
if message.ID == 0 {
|
if message.ID == 0 {
|
||||||
saved = lipgloss.NewStyle().Faint(true).Render(" (not saved)")
|
saved = lipgloss.NewStyle().Faint(true).Render(" (not saved)")
|
||||||
}
|
}
|
||||||
|
|
||||||
// write message heading with space for content
|
var selectedPrefix string
|
||||||
header := fmt.Sprintf(" %s", style.Render(icon+friendly)+saved)
|
if m.focus == focusMessages && i == m.selectedMessage {
|
||||||
|
selectedPrefix = "> "
|
||||||
|
}
|
||||||
|
|
||||||
|
header := lipgloss.NewStyle().PaddingLeft(1).Render(selectedPrefix + user + saved)
|
||||||
sb.WriteString(header)
|
sb.WriteString(header)
|
||||||
|
lineCnt += lipgloss.Height(header)
|
||||||
|
|
||||||
// TODO: special rendering for tool calls/results?
|
// TODO: special rendering for tool calls/results?
|
||||||
if message.Content != "" {
|
if message.Content != "" {
|
||||||
sb.WriteString("\n\n")
|
sb.WriteString("\n\n")
|
||||||
|
lineCnt += 1
|
||||||
|
|
||||||
// write message contents
|
// write message contents
|
||||||
var highlighted string
|
var highlighted string
|
||||||
@ -672,10 +727,12 @@ func (m *model) conversationView() string {
|
|||||||
}
|
}
|
||||||
contents := messageStyle.Width(m.content.Width).Render(highlighted)
|
contents := messageStyle.Width(m.content.Width).Render(highlighted)
|
||||||
sb.WriteString(contents)
|
sb.WriteString(contents)
|
||||||
|
lineCnt += lipgloss.Height(contents)
|
||||||
}
|
}
|
||||||
|
|
||||||
if i < msgCnt-1 {
|
if i < msgCnt-1 {
|
||||||
sb.WriteString("\n\n")
|
sb.WriteString("\n\n")
|
||||||
|
lineCnt += 1
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return sb.String()
|
return sb.String()
|
||||||
|
Loading…
Reference in New Issue
Block a user