lmcli/pkg/tui/views/chat/input.go

212 lines
5.1 KiB
Go

package chat
import (
"fmt"
"strings"
"git.mlow.ca/mlow/lmcli/pkg/api"
"git.mlow.ca/mlow/lmcli/pkg/conversation"
"git.mlow.ca/mlow/lmcli/pkg/tui/model"
"git.mlow.ca/mlow/lmcli/pkg/tui/shared"
tuiutil "git.mlow.ca/mlow/lmcli/pkg/tui/util"
tea "github.com/charmbracelet/bubbletea"
)
func (m *Model) handleInput(msg tea.KeyMsg) tea.Cmd {
switch m.focus {
case focusInput:
cmd := m.handleInputKey(msg)
if cmd != nil {
return cmd
}
case focusMessages:
cmd := m.handleMessagesKey(msg)
if cmd != nil {
return cmd
}
}
switch msg.String() {
case "esc":
if m.state == pendingResponse {
m.stopSignal <- struct{}{}
return shared.KeyHandled(msg)
}
return func() tea.Msg {
return shared.MsgViewChange(shared.ViewConversations)
}
case "ctrl+c":
if m.state == pendingResponse {
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.showDetails = !m.showDetails
if !m.showDetails && m.selectedMessage == 0 {
if len(m.App.Messages) > 1 {
m.selectedMessage = 1
} else {
m.selectedMessage = -1
}
}
m.rebuildMessageCache()
m.updateContent()
return shared.KeyHandled(msg)
case "ctrl+w":
m.wrap = !m.wrap
m.rebuildMessageCache()
m.updateContent()
return shared.KeyHandled(msg)
case "ctrl+n":
m.App.NewConversation()
m.rebuildMessageCache()
m.updateContent()
return shared.KeyHandled(msg)
}
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() {
case "tab", "enter":
m.focus = focusInput
m.updateContent()
m.input.Focus()
return shared.KeyHandled(msg)
case "e":
if m.selectedMessage < len(m.App.Messages) {
m.editorTarget = selectedMessage
return tuiutil.OpenTempfileEditor(
"message.*.md",
m.App.Messages[m.selectedMessage].Content,
"# Edit the message below\n",
)
}
return nil
case "ctrl+k", "ctrl+up":
if m.selectedMessage > 0 {
m.scrollSelection(-1)
}
return shared.KeyHandled(msg)
case "ctrl+j", "ctrl+down":
if m.selectedMessage < len(m.App.Messages)-1 {
m.scrollSelection(1)
}
return shared.KeyHandled(msg)
case "ctrl+h", "ctrl+left", "ctrl+l", "ctrl+right":
dir := model.CyclePrev
if msg.String() == "ctrl+l" || msg.String() == "ctrl+right" {
dir = model.CycleNext
}
var cmd tea.Cmd
if m.selectedMessage == 0 {
cmd = m.cycleSelectedRoot(&m.App.Conversation, dir)
} else if m.selectedMessage > 0 {
cmd = m.cycleSelectedReply(&m.App.Messages[m.selectedMessage-1], dir)
}
return cmd
case "ctrl+r":
// 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]
cmd := m.promptLLM()
m.updateContent()
m.content.GotoBottom()
return cmd
}
}
return nil
}
// handleInputKey handles input when the input textarea is focused
func (m *Model) handleInputKey(msg tea.KeyMsg) tea.Cmd {
switch msg.String() {
case "esc":
m.focus = focusMessages
if len(m.App.Messages) > 0 {
if m.selectedMessage < 0 || m.selectedMessage >= len(m.App.Messages) {
m.selectedMessage = len(m.App.Messages) - 1
}
offset := m.messageOffsets[m.selectedMessage]
tuiutil.ScrollIntoView(&m.content, offset, m.content.Height/2)
} else {
m.selectedMessage = -1
m.content.GotoTop()
}
m.updateContent()
m.input.Blur()
return shared.KeyHandled(msg)
case "ctrl+s":
if m.state != idle {
return nil
}
input := strings.TrimSpace(m.input.Value())
if input == "" {
return shared.KeyHandled(msg)
}
if len(m.App.Messages) > 0 && m.App.Messages[len(m.App.Messages)-1].Role.IsUser() {
return shared.WrapError(fmt.Errorf("Can't reply to a user message"))
}
m.addMessage(conversation.Message{
Role: api.MessageRoleUser,
Content: input,
})
m.input.SetValue("")
var cmds []tea.Cmd
if m.persistence {
cmds = append(cmds, m.persistConversation())
}
cmds = append(cmds, m.promptLLM())
m.updateContent()
m.content.GotoBottom()
return tea.Batch(cmds...)
case "ctrl+e":
cmd := tuiutil.OpenTempfileEditor("message.*.md", m.input.Value(), "# Edit your input below\n")
m.editorTarget = input
return cmd
}
return nil
}