package chat import ( "fmt" "strings" models "git.mlow.ca/mlow/lmcli/pkg/lmcli/model" "git.mlow.ca/mlow/lmcli/pkg/tui/shared" tuiutil "git.mlow.ca/mlow/lmcli/pkg/tui/util" tea "github.com/charmbracelet/bubbletea" ) type MessageCycleDirection int const ( CycleNext MessageCycleDirection = 1 CyclePrev MessageCycleDirection = -1 ) func (m *Model) HandleInput(msg tea.KeyMsg) (bool, tea.Cmd) { switch m.focus { case focusInput: consumed, cmd := m.handleInputKey(msg) if consumed { return true, cmd } case focusMessages: consumed, cmd := m.handleMessagesKey(msg) if consumed { return true, cmd } } switch msg.String() { case "esc": if m.waitingForReply { m.stopSignal <- struct{}{} return true, nil } return true, func() tea.Msg { return shared.MsgViewChange(shared.StateConversations) } case "ctrl+c": if m.waitingForReply { m.stopSignal <- struct{}{} return true, nil } case "ctrl+p": m.persistence = !m.persistence return true, nil case "ctrl+t": m.showToolResults = !m.showToolResults m.rebuildMessageCache() m.updateContent() return true, nil case "ctrl+w": m.wrap = !m.wrap m.rebuildMessageCache() m.updateContent() return true, nil } return false, nil } // handleMessagesKey handles input when the messages pane is focused func (m *Model) handleMessagesKey(msg tea.KeyMsg) (bool, tea.Cmd) { switch msg.String() { case "tab", "enter": m.focus = focusInput m.updateContent() m.input.Focus() return true, nil case "e": if m.selectedMessage < len(m.messages) { m.editorTarget = selectedMessage return true, tuiutil.OpenTempfileEditor( "message.*.md", m.messages[m.selectedMessage].Content, "# Edit the message below\n", ) } return false, nil case "ctrl+k": if m.selectedMessage > 0 && len(m.messages) == len(m.messageOffsets) { m.selectedMessage-- m.updateContent() offset := m.messageOffsets[m.selectedMessage] tuiutil.ScrollIntoView(&m.content, offset, m.content.Height/2) } return true, nil 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] tuiutil.ScrollIntoView(&m.content, offset, m.content.Height/2) } return true, nil case "ctrl+h", "ctrl+l": dir := CyclePrev if msg.String() == "ctrl+l" { dir = CycleNext } var cmd tea.Cmd if m.selectedMessage == 0 { cmd = m.cycleSelectedRoot(m.conversation, dir) } else if m.selectedMessage > 0 { cmd = m.cycleSelectedReply(&m.messages[m.selectedMessage-1], dir) } return cmd != nil, cmd case "ctrl+r": // resubmit the conversation with all messages up until and including the selected message if m.waitingForReply || len(m.messages) == 0 { return true, nil } m.messages = m.messages[:m.selectedMessage+1] m.messageCache = m.messageCache[:m.selectedMessage+1] cmd := m.promptLLM() m.updateContent() m.content.GotoBottom() return true, cmd } return false, nil } // handleInputKey handles input when the input textarea is focused func (m *Model) handleInputKey(msg tea.KeyMsg) (bool, tea.Cmd) { switch msg.String() { case "esc": m.focus = focusMessages if len(m.messages) > 0 { if m.selectedMessage < 0 || m.selectedMessage >= len(m.messages) { m.selectedMessage = len(m.messages) - 1 } offset := m.messageOffsets[m.selectedMessage] tuiutil.ScrollIntoView(&m.content, offset, m.content.Height/2) } m.updateContent() m.input.Blur() return true, nil case "ctrl+s": // TODO: call a "handleSend" function with returns a tea.Cmd if m.waitingForReply { return false, nil } input := strings.TrimSpace(m.input.Value()) if input == "" { return true, nil } if len(m.messages) > 0 && m.messages[len(m.messages)-1].Role == models.MessageRoleUser { return true, shared.WrapError(fmt.Errorf("Can't reply to a user message")) } m.addMessage(models.Message{ Role: models.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 true, tea.Batch(cmds...) case "ctrl+e": cmd := tuiutil.OpenTempfileEditor("message.*.md", m.input.Value(), "# Edit your input below\n") m.editorTarget = input return true, cmd } return false, nil }