tui: update/clean up input handling
This commit is contained in:
parent
e1970a315a
commit
105ee2e01b
@ -161,37 +161,48 @@ var (
|
|||||||
footerStyle = lipgloss.NewStyle()
|
footerStyle = lipgloss.NewStyle()
|
||||||
)
|
)
|
||||||
|
|
||||||
func (m *chatModel) handleInput(msg tea.KeyMsg) tea.Cmd {
|
func (m *chatModel) handleInput(msg tea.KeyMsg) (bool, tea.Cmd) {
|
||||||
switch m.focus {
|
switch m.focus {
|
||||||
case focusInput:
|
case focusInput:
|
||||||
cmd := m.handleInputKey(msg)
|
consumed, cmd := m.handleInputKey(msg)
|
||||||
if cmd != nil {
|
if consumed {
|
||||||
return cmd
|
return true, cmd
|
||||||
}
|
}
|
||||||
case focusMessages:
|
case focusMessages:
|
||||||
cmd := m.handleMessagesKey(msg)
|
consumed, cmd := m.handleMessagesKey(msg)
|
||||||
if cmd != nil {
|
if consumed {
|
||||||
return cmd
|
return true, cmd
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
switch msg.String() {
|
switch msg.String() {
|
||||||
case "esc":
|
case "esc":
|
||||||
return func() tea.Msg {
|
return true, func() tea.Msg {
|
||||||
|
return msgChangeState(stateConversations)
|
||||||
|
}
|
||||||
|
case "ctrl+c":
|
||||||
|
if m.waitingForReply {
|
||||||
|
m.stopSignal <- struct{}{}
|
||||||
|
return true, nil
|
||||||
|
}
|
||||||
|
return true, func() tea.Msg {
|
||||||
return msgChangeState(stateConversations)
|
return msgChangeState(stateConversations)
|
||||||
}
|
}
|
||||||
case "ctrl+p":
|
case "ctrl+p":
|
||||||
m.persistence = !m.persistence
|
m.persistence = !m.persistence
|
||||||
|
return true, nil
|
||||||
case "ctrl+t":
|
case "ctrl+t":
|
||||||
m.showToolResults = !m.showToolResults
|
m.showToolResults = !m.showToolResults
|
||||||
m.rebuildMessageCache()
|
m.rebuildMessageCache()
|
||||||
m.updateContent()
|
m.updateContent()
|
||||||
|
return true, nil
|
||||||
case "ctrl+w":
|
case "ctrl+w":
|
||||||
m.wrap = !m.wrap
|
m.wrap = !m.wrap
|
||||||
m.rebuildMessageCache()
|
m.rebuildMessageCache()
|
||||||
m.updateContent()
|
m.updateContent()
|
||||||
|
return true, nil
|
||||||
}
|
}
|
||||||
return nil
|
return false, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m chatModel) Init() tea.Cmd {
|
func (m chatModel) Init() tea.Cmd {
|
||||||
@ -375,17 +386,18 @@ func (m chatModel) Update(msg tea.Msg) (chatModel, tea.Cmd) {
|
|||||||
return m, tea.Batch(cmds...)
|
return m, tea.Batch(cmds...)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *chatModel) handleMessagesKey(msg tea.KeyMsg) tea.Cmd {
|
func (m *chatModel) handleMessagesKey(msg tea.KeyMsg) (bool, tea.Cmd) {
|
||||||
switch msg.String() {
|
switch msg.String() {
|
||||||
case "tab":
|
case "tab", "enter":
|
||||||
m.focus = focusInput
|
m.focus = focusInput
|
||||||
m.updateContent()
|
m.updateContent()
|
||||||
m.input.Focus()
|
m.input.Focus()
|
||||||
|
return true, nil
|
||||||
case "e":
|
case "e":
|
||||||
message := m.messages[m.selectedMessage]
|
message := m.messages[m.selectedMessage]
|
||||||
cmd := openTempfileEditor("message.*.md", message.Content, "# Edit the message below\n")
|
cmd := openTempfileEditor("message.*.md", message.Content, "# Edit the message below\n")
|
||||||
m.editorTarget = selectedMessage
|
m.editorTarget = selectedMessage
|
||||||
return cmd
|
return true, cmd
|
||||||
case "ctrl+k":
|
case "ctrl+k":
|
||||||
if m.selectedMessage > 0 && len(m.messages) == len(m.messageOffsets) {
|
if m.selectedMessage > 0 && len(m.messages) == len(m.messageOffsets) {
|
||||||
m.selectedMessage--
|
m.selectedMessage--
|
||||||
@ -393,6 +405,7 @@ func (m *chatModel) handleMessagesKey(msg tea.KeyMsg) tea.Cmd {
|
|||||||
offset := m.messageOffsets[m.selectedMessage]
|
offset := m.messageOffsets[m.selectedMessage]
|
||||||
scrollIntoView(&m.content, offset, 0.1)
|
scrollIntoView(&m.content, offset, 0.1)
|
||||||
}
|
}
|
||||||
|
return true, nil
|
||||||
case "ctrl+j":
|
case "ctrl+j":
|
||||||
if m.selectedMessage < len(m.messages)-1 && len(m.messages) == len(m.messageOffsets) {
|
if m.selectedMessage < len(m.messages)-1 && len(m.messages) == len(m.messageOffsets) {
|
||||||
m.selectedMessage++
|
m.selectedMessage++
|
||||||
@ -400,44 +413,43 @@ func (m *chatModel) handleMessagesKey(msg tea.KeyMsg) tea.Cmd {
|
|||||||
offset := m.messageOffsets[m.selectedMessage]
|
offset := m.messageOffsets[m.selectedMessage]
|
||||||
scrollIntoView(&m.content, offset, 0.1)
|
scrollIntoView(&m.content, offset, 0.1)
|
||||||
}
|
}
|
||||||
|
return true, nil
|
||||||
case "ctrl+r":
|
case "ctrl+r":
|
||||||
// resubmit the conversation with all messages up until and including the selected message
|
// resubmit the conversation with all messages up until and including the selected message
|
||||||
if m.waitingForReply || len(m.messages) == 0 {
|
if m.waitingForReply || len(m.messages) == 0 {
|
||||||
return nil
|
return true, nil
|
||||||
}
|
}
|
||||||
m.messages = m.messages[:m.selectedMessage+1]
|
m.messages = m.messages[:m.selectedMessage+1]
|
||||||
m.messageCache = m.messageCache[:m.selectedMessage+1]
|
m.messageCache = m.messageCache[:m.selectedMessage+1]
|
||||||
m.updateContent()
|
m.updateContent()
|
||||||
m.content.GotoBottom()
|
m.content.GotoBottom()
|
||||||
return m.promptLLM()
|
return true, m.promptLLM()
|
||||||
}
|
}
|
||||||
return nil
|
return false, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *chatModel) handleInputKey(msg tea.KeyMsg) tea.Cmd {
|
func (m *chatModel) handleInputKey(msg tea.KeyMsg) (bool, tea.Cmd) {
|
||||||
switch msg.String() {
|
switch msg.String() {
|
||||||
case "esc":
|
case "esc":
|
||||||
return func() tea.Msg {
|
m.focus = focusMessages
|
||||||
return msgChangeState(stateConversations)
|
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]
|
||||||
|
scrollIntoView(&m.content, offset, 0.1)
|
||||||
}
|
}
|
||||||
//m.focus = focusMessages
|
m.updateContent()
|
||||||
//if len(m.messages) > 0 {
|
m.input.Blur()
|
||||||
// if m.selectedMessage < 0 || m.selectedMessage >= len(m.messages) {
|
return true, nil
|
||||||
// m.selectedMessage = len(m.messages) - 1
|
|
||||||
// }
|
|
||||||
// offset := m.messageOffsets[m.selectedMessage]
|
|
||||||
// scrollIntoView(&m.content, offset, 0.1)
|
|
||||||
//}
|
|
||||||
//m.updateContent()
|
|
||||||
//m.input.Blur()
|
|
||||||
case "ctrl+s":
|
case "ctrl+s":
|
||||||
userInput := strings.TrimSpace(m.input.Value())
|
userInput := strings.TrimSpace(m.input.Value())
|
||||||
if strings.TrimSpace(userInput) == "" {
|
if strings.TrimSpace(userInput) == "" {
|
||||||
return nil
|
return true, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(m.messages) > 0 && m.messages[len(m.messages)-1].Role == models.MessageRoleUser {
|
if len(m.messages) > 0 && m.messages[len(m.messages)-1].Role == models.MessageRoleUser {
|
||||||
return wrapError(fmt.Errorf("Can't reply to a user message"))
|
return true, wrapError(fmt.Errorf("Can't reply to a user message"))
|
||||||
}
|
}
|
||||||
|
|
||||||
reply := models.Message{
|
reply := models.Message{
|
||||||
@ -451,18 +463,18 @@ func (m *chatModel) handleInputKey(msg tea.KeyMsg) tea.Cmd {
|
|||||||
err = m.ctx.Store.SaveConversation(m.conversation)
|
err = m.ctx.Store.SaveConversation(m.conversation)
|
||||||
}
|
}
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return wrapError(err)
|
return true, wrapError(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// ensure all messages up to the one we're about to add are persisted
|
// ensure all messages up to the one we're about to add are persisted
|
||||||
cmd := m.persistConversation()
|
cmd := m.persistConversation()
|
||||||
if cmd != nil {
|
if cmd != nil {
|
||||||
return cmd
|
return true, cmd
|
||||||
}
|
}
|
||||||
|
|
||||||
savedReply, err := m.ctx.Store.AddReply(m.conversation, reply)
|
savedReply, err := m.ctx.Store.AddReply(m.conversation, reply)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return wrapError(err)
|
return true, wrapError(err)
|
||||||
}
|
}
|
||||||
reply = *savedReply
|
reply = *savedReply
|
||||||
}
|
}
|
||||||
@ -472,13 +484,13 @@ func (m *chatModel) handleInputKey(msg tea.KeyMsg) tea.Cmd {
|
|||||||
|
|
||||||
m.updateContent()
|
m.updateContent()
|
||||||
m.content.GotoBottom()
|
m.content.GotoBottom()
|
||||||
return m.promptLLM()
|
return true, m.promptLLM()
|
||||||
case "ctrl+e":
|
case "ctrl+e":
|
||||||
cmd := openTempfileEditor("message.*.md", m.input.Value(), "# Edit your input below\n")
|
cmd := openTempfileEditor("message.*.md", m.input.Value(), "# Edit your input below\n")
|
||||||
m.editorTarget = input
|
m.editorTarget = input
|
||||||
return cmd
|
return true, cmd
|
||||||
}
|
}
|
||||||
return nil
|
return false, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *chatModel) renderMessageHeading(i int, message *models.Message) string {
|
func (m *chatModel) renderMessageHeading(i int, message *models.Message) string {
|
||||||
|
@ -41,11 +41,11 @@ func newConversationsModel(tui *model) conversationsModel {
|
|||||||
return m
|
return m
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *conversationsModel) handleInput(msg tea.KeyMsg) tea.Cmd {
|
func (m *conversationsModel) handleInput(msg tea.KeyMsg) (bool, tea.Cmd) {
|
||||||
switch msg.String() {
|
switch msg.String() {
|
||||||
case "enter":
|
case "enter":
|
||||||
// how to notify chats model
|
// how to notify chats model
|
||||||
return func() tea.Msg {
|
return true, func() tea.Msg {
|
||||||
return msgChangeState(stateChat)
|
return msgChangeState(stateChat)
|
||||||
}
|
}
|
||||||
case "n":
|
case "n":
|
||||||
@ -59,7 +59,7 @@ func (m *conversationsModel) handleInput(msg tea.KeyMsg) tea.Cmd {
|
|||||||
case "shift+r":
|
case "shift+r":
|
||||||
// show prompt to generate name for conversation
|
// show prompt to generate name for conversation
|
||||||
}
|
}
|
||||||
return nil
|
return false, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m conversationsModel) Init() tea.Cmd {
|
func (m conversationsModel) Init() tea.Cmd {
|
||||||
|
@ -2,7 +2,6 @@ package tui
|
|||||||
|
|
||||||
// The terminal UI for lmcli, launched from the `lmcli chat` command
|
// The terminal UI for lmcli, launched from the `lmcli chat` command
|
||||||
// TODO:
|
// TODO:
|
||||||
// - conversation list view
|
|
||||||
// - change model
|
// - change model
|
||||||
// - rename conversation
|
// - rename conversation
|
||||||
// - set system prompt
|
// - set system prompt
|
||||||
@ -83,28 +82,33 @@ func (m model) Init() tea.Cmd {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *model) handleGlobalInput(msg tea.KeyMsg) tea.Cmd {
|
func (m *model) handleGlobalInput(msg tea.KeyMsg) (bool, tea.Cmd) {
|
||||||
switch msg.String() {
|
// delegate input to the active child state first, only handling it at the
|
||||||
case "ctrl+c":
|
// global level if the child state does not
|
||||||
if m.chat.waitingForReply {
|
var cmds []tea.Cmd
|
||||||
m.chat.stopSignal <- struct{}{}
|
switch m.state {
|
||||||
return nil
|
case stateChat:
|
||||||
} else {
|
handled, cmd := m.chat.handleInput(msg)
|
||||||
return tea.Quit
|
cmds = append(cmds, cmd)
|
||||||
|
if handled {
|
||||||
|
m.chat, cmd = m.chat.Update(nil)
|
||||||
|
cmds = append(cmds, cmd)
|
||||||
|
return true, tea.Batch(cmds...)
|
||||||
}
|
}
|
||||||
case "q":
|
case stateConversations:
|
||||||
if m.chat.focus != focusInput {
|
handled, cmd := m.conversations.handleInput(msg)
|
||||||
return tea.Quit
|
cmds = append(cmds, cmd)
|
||||||
}
|
if handled {
|
||||||
default:
|
m.conversations, cmd = m.conversations.Update(nil)
|
||||||
switch m.state {
|
cmds = append(cmds, cmd)
|
||||||
case stateChat:
|
return true, tea.Batch(cmds...)
|
||||||
return m.chat.handleInput(msg)
|
|
||||||
case stateConversations:
|
|
||||||
return m.conversations.handleInput(msg)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return nil
|
switch msg.String() {
|
||||||
|
case "ctrl+c", "ctrl+q":
|
||||||
|
return true, tea.Quit
|
||||||
|
}
|
||||||
|
return false, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||||
@ -112,8 +116,8 @@ func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
|||||||
|
|
||||||
switch msg := msg.(type) {
|
switch msg := msg.(type) {
|
||||||
case tea.KeyMsg:
|
case tea.KeyMsg:
|
||||||
cmd := m.handleGlobalInput(msg)
|
handled, cmd := m.handleGlobalInput(msg)
|
||||||
if cmd != nil {
|
if handled {
|
||||||
return m, cmd
|
return m, cmd
|
||||||
}
|
}
|
||||||
case msgChangeState:
|
case msgChangeState:
|
||||||
|
Loading…
Reference in New Issue
Block a user