212 lines
5.1 KiB
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
|
|
}
|