Matt Low
bb48bc9abd
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
202 lines
4.9 KiB
Go
202 lines
4.9 KiB
Go
package chat
|
|
|
|
import (
|
|
"fmt"
|
|
"strings"
|
|
|
|
"git.mlow.ca/mlow/lmcli/pkg/api"
|
|
"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
|
|
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)
|
|
}
|
|
m.updateContent()
|
|
m.input.Blur()
|
|
return shared.KeyHandled(msg)
|
|
case "ctrl+s":
|
|
// TODO: call a "handleSend" function which returns a tea.Cmd
|
|
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 == api.MessageRoleUser {
|
|
return shared.WrapError(fmt.Errorf("Can't reply to a user message"))
|
|
}
|
|
|
|
m.addMessage(api.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
|
|
}
|