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 }