diff --git a/pkg/tui/views/chat/chat.go b/pkg/tui/views/chat/chat.go index d3113f7..9c08bf7 100644 --- a/pkg/tui/views/chat/chat.go +++ b/pkg/tui/views/chat/chat.go @@ -13,20 +13,6 @@ import ( "github.com/charmbracelet/lipgloss" ) -type focusState int - -const ( - focusInput focusState = iota - focusMessages -) - -type editorTarget int - -const ( - input editorTarget = iota - selectedMessage -) - // custom tea.Msg types type ( // sent on each chunk received from LLM @@ -61,16 +47,38 @@ type ( msgMessageCloned *models.Message ) +type focusState int + +const ( + focusInput focusState = iota + focusMessages +) + +type editorTarget int + +const ( + input editorTarget = iota + selectedMessage +) + +type state int + +const ( + idle state = iota + loading + pendingResponse +) + type Model struct { shared.State shared.Sections // app state + state state // current overall status of the view conversation *models.Conversation rootMessages []models.Message messages []models.Message selectedMessage int - waitingForReply bool editorTarget editorTarget stopSignal chan struct{} replyChan chan models.Message @@ -80,7 +88,6 @@ type Model struct { // ui state focus focusState wrap bool // whether message content is wrapped to viewport width - status string // a general status message showToolResults bool // whether tool calls and results are shown messageCache []string // cache of syntax highlighted and wrapped message content messageOffsets []int @@ -101,6 +108,7 @@ func Chat(state shared.State) Model { m := Model{ State: state, + state: idle, conversation: &models.Conversation{}, persistence: true, @@ -150,8 +158,6 @@ func Chat(state shared.State) Model { m.input.FocusedStyle.Base = inputFocusedStyle m.input.BlurredStyle.Base = inputBlurredStyle - m.waitingForReply = false - m.status = "Press ctrl+s to send" return m } diff --git a/pkg/tui/views/chat/conversation.go b/pkg/tui/views/chat/conversation.go index 0c098bf..5c3a71e 100644 --- a/pkg/tui/views/chat/conversation.go +++ b/pkg/tui/views/chat/conversation.go @@ -244,9 +244,8 @@ func (m *Model) persistConversation() tea.Cmd { } func (m *Model) promptLLM() tea.Cmd { - m.waitingForReply = true + m.state = pendingResponse m.replyCursor.Blink = false - m.status = "Press ctrl+c to cancel" m.tokenCount = 0 m.startTime = time.Now() diff --git a/pkg/tui/views/chat/input.go b/pkg/tui/views/chat/input.go index 6bc44f2..ce73d88 100644 --- a/pkg/tui/views/chat/input.go +++ b/pkg/tui/views/chat/input.go @@ -33,7 +33,7 @@ func (m *Model) HandleInput(msg tea.KeyMsg) (bool, tea.Cmd) { switch msg.String() { case "esc": - if m.waitingForReply { + if m.state == pendingResponse { m.stopSignal <- struct{}{} return true, nil } @@ -41,7 +41,7 @@ func (m *Model) HandleInput(msg tea.KeyMsg) (bool, tea.Cmd) { return shared.MsgViewChange(shared.StateConversations) } case "ctrl+c": - if m.waitingForReply { + if m.state == pendingResponse { m.stopSignal <- struct{}{} return true, nil } @@ -112,15 +112,14 @@ func (m *Model) handleMessagesKey(msg tea.KeyMsg) (bool, tea.Cmd) { 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 + if m.state == idle && m.selectedMessage < len(m.messages) { + 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 } - 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 } @@ -141,8 +140,8 @@ func (m *Model) handleInputKey(msg tea.KeyMsg) (bool, tea.Cmd) { m.input.Blur() return true, nil case "ctrl+s": - // TODO: call a "handleSend" function with returns a tea.Cmd - if m.waitingForReply { + // TODO: call a "handleSend" function which returns a tea.Cmd + if m.state != idle { return false, nil } diff --git a/pkg/tui/views/chat/update.go b/pkg/tui/views/chat/update.go index 72eba8d..8992440 100644 --- a/pkg/tui/views/chat/update.go +++ b/pkg/tui/views/chat/update.go @@ -142,17 +142,15 @@ func (m Model) Update(msg tea.Msg) (Model, tea.Cmd) { m.updateContent() case msgResponseEnd: - m.waitingForReply = false + m.state = idle last := len(m.messages) - 1 if last < 0 { panic("Unexpected empty messages handling msgResponseEnd") } m.setMessageContents(last, strings.TrimSpace(m.messages[last].Content)) m.updateContent() - m.status = "Press ctrl+s to send" case msgResponseError: - m.waitingForReply = false - m.status = "Press ctrl+s to send" + m.state = idle m.State.Err = error(msg) m.updateContent() case msgConversationTitleGenerated: @@ -162,7 +160,7 @@ func (m Model) Update(msg tea.Msg) (Model, tea.Cmd) { cmds = append(cmds, m.updateConversationTitle(m.conversation)) } case cursor.BlinkMsg: - if m.waitingForReply { + if m.state == pendingResponse { // ensure we show the updated "wait for response" cursor blink state m.updateContent() } diff --git a/pkg/tui/views/chat/view.go b/pkg/tui/views/chat/view.go index b454757..1fc0fd6 100644 --- a/pkg/tui/views/chat/view.go +++ b/pkg/tui/views/chat/view.go @@ -139,7 +139,7 @@ func (m *Model) renderMessage(i int) string { } // Show the assistant's cursor - if m.waitingForReply && i == len(m.messages)-1 && msg.Role == models.MessageRoleAssistant { + if m.state == pendingResponse && i == len(m.messages)-1 && msg.Role == models.MessageRoleAssistant { sb.WriteString(m.replyCursor.View()) } @@ -237,7 +237,7 @@ func (m *Model) conversationMessagesView() string { lineCnt += lipgloss.Height(heading) var rendered string - if m.waitingForReply && i == len(m.messages)-1 { + if m.state == pendingResponse && i == len(m.messages)-1 { // do a direct render of final (assistant) message to handle the // assistant cursor blink rendered = m.renderMessage(i) @@ -251,7 +251,7 @@ func (m *Model) conversationMessagesView() string { } // Render a placeholder for the incoming assistant reply - if m.waitingForReply && (len(m.messages) == 0 || m.messages[len(m.messages)-1].Role != models.MessageRoleAssistant) { + if m.state == pendingResponse && (len(m.messages) == 0 || m.messages[len(m.messages)-1].Role != models.MessageRoleAssistant) { heading := m.renderMessageHeading(-1, &models.Message{ Role: models.MessageRoleAssistant, }) @@ -289,9 +289,12 @@ func (m *Model) footerView() string { saving = savingStyle.Foreground(lipgloss.Color("1")).Render("❌💾") } - status := m.status - if m.waitingForReply { - status += m.spinner.View() + var status string + switch m.state { + case pendingResponse: + status = "Press ctrl+c to cancel" + m.spinner.View() + default: + status = "Press ctrl+s to send" } leftSegments := []string{