Compare commits

..

5 Commits

Author SHA1 Message Date
e9fde37201 tui: fixed response cancelling 2024-03-15 06:47:07 +00:00
6242ea17d8 tui: ctrl+r to retry previous message 2024-03-14 17:56:03 +00:00
2ca94e1ffb tui: fixed footer styling 2024-03-14 17:55:31 +00:00
2b0d474660 tui: removed scrollbar 2024-03-14 17:55:21 +00:00
fdf8033aff tui: minor fixed and cleanup 2024-03-14 06:39:25 +00:00

View File

@ -44,9 +44,9 @@ type model struct {
conversation *models.Conversation conversation *models.Conversation
messages []models.Message messages []models.Message
waitingForReply bool waitingForReply bool
stopSignal chan interface{}
replyChan chan models.Message replyChan chan models.Message
replyChunkChan chan string replyChunkChan chan string
replyCancelFunc context.CancelFunc
err error err error
persistence bool // whether we will save new messages in the conversation persistence bool // whether we will save new messages in the conversation
@ -124,7 +124,7 @@ func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
switch msg.String() { switch msg.String() {
case "ctrl+c": case "ctrl+c":
if m.waitingForReply { if m.waitingForReply {
m.replyCancelFunc() m.stopSignal <- "stahp!"
} else { } else {
return m, tea.Quit return m, tea.Quit
} }
@ -204,11 +204,9 @@ func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
m.updateContent() m.updateContent()
cmds = append(cmds, m.waitForReply()) cmds = append(cmds, m.waitForReply())
case msgResponseEnd: case msgResponseEnd:
m.replyCancelFunc = nil
m.waitingForReply = false m.waitingForReply = false
m.status = "Press ctrl+s to send" m.status = "Press ctrl+s to send"
case msgResponseError: case msgResponseError:
m.replyCancelFunc = nil
m.waitingForReply = false m.waitingForReply = false
m.status = "Press ctrl+s to send" m.status = "Press ctrl+s to send"
m.err = error(msg) m.err = error(msg)
@ -225,7 +223,6 @@ func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
m.err = error(msg) m.err = error(msg)
} }
var cmd tea.Cmd var cmd tea.Cmd
m.spinner, cmd = m.spinner.Update(msg) m.spinner, cmd = m.spinner.Update(msg)
if cmd != nil { if cmd != nil {
@ -250,23 +247,17 @@ func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
} }
func (m model) View() string { func (m model) View() string {
if m.content.Width == 0 { if m.width == 0 {
// this is the case upon initial startup, but it's also a safe bet that // this is the case upon initial startup, but it's also a safe bet that
// we can just skip rendering if the terminal is really 0 width... // we can just skip rendering if the terminal is really 0 width...
// without this, the below view functions may do weird things // without this, the m.*View() functions may crash
return "" return ""
} }
m.content.Height = m.height - m.getFixedComponentHeight()
sections := make([]string, 0, 6) sections := make([]string, 0, 6)
error := m.errorView()
scrollbar := m.scrollbarView()
sections = append(sections, m.headerView()) sections = append(sections, m.headerView())
if scrollbar != "" {
sections = append(sections, scrollbar)
}
sections = append(sections, m.contentView()) sections = append(sections, m.contentView())
error := m.errorView()
if error != "" { if error != "" {
sections = append(sections, error) sections = append(sections, error)
} }
@ -279,15 +270,13 @@ func (m model) View() string {
) )
} }
// returns the total height of "fixed" components, which are those which don't
// change height dependent on window size.
func (m *model) getFixedComponentHeight() int { func (m *model) getFixedComponentHeight() int {
h := 0 h := 0
h += m.input.Height() h += m.input.Height()
h += lipgloss.Height(m.headerView()) h += lipgloss.Height(m.headerView())
h += lipgloss.Height(m.footerView()) h += lipgloss.Height(m.footerView())
scrollbar := m.scrollbarView()
if scrollbar != "" {
h += lipgloss.Height(scrollbar)
}
errorView := m.errorView() errorView := m.errorView()
if errorView != "" { if errorView != "" {
h += lipgloss.Height(errorView) h += lipgloss.Height(errorView)
@ -327,20 +316,6 @@ func (m *model) errorView() string {
Render(fmt.Sprintf("%s", m.err)) Render(fmt.Sprintf("%s", m.err))
} }
func (m *model) scrollbarView() string {
if m.content.AtTop() {
return ""
}
count := int(m.content.ScrollPercent() * float64(m.width-2))
fill := strings.Repeat("-", count)
return lipgloss.NewStyle().
Width(m.width).
PaddingLeft(1).
PaddingRight(1).
Render(fill)
}
func (m *model) inputView() string { func (m *model) inputView() string {
return m.input.View() return m.input.View()
} }
@ -349,11 +324,12 @@ func (m *model) footerView() string {
segmentStyle := lipgloss.NewStyle().PaddingLeft(1).PaddingRight(1).Faint(true) segmentStyle := lipgloss.NewStyle().PaddingLeft(1).PaddingRight(1).Faint(true)
segmentSeparator := "|" segmentSeparator := "|"
savingStyle := segmentStyle.Copy().Bold(true)
saving := "" saving := ""
if m.persistence { if m.persistence {
saving = segmentStyle.Copy().Bold(true).Foreground(lipgloss.Color("2")).Render("✅💾") saving = savingStyle.Foreground(lipgloss.Color("2")).Render("✅💾")
} else { } else {
saving = segmentStyle.Copy().Bold(true).Foreground(lipgloss.Color("1")).Render("❌💾") saving = savingStyle.Foreground(lipgloss.Color("1")).Render("❌💾")
} }
status := m.status status := m.status
@ -383,9 +359,11 @@ func (m *model) footerView() string {
footer := left + padding + right footer := left + padding + right
if remaining < 0 { if remaining < 0 {
ellipses := "... " ellipses := "... "
footer = footer[:m.width-len(ellipses)] + ellipses // this doesn't work very well, due to trying to trim a string with
// ansii chars already in it
footer = footer[:(len(footer)+remaining)-len(ellipses)-3] + ellipses
} }
return footerStyle.Render(footer) return footerStyle.Width(m.width).Render(footer)
} }
func initialModel(ctx *lmcli.Context, convShortname string) model { func initialModel(ctx *lmcli.Context, convShortname string) model {
@ -395,6 +373,7 @@ func initialModel(ctx *lmcli.Context, convShortname string) model {
conversation: &models.Conversation{}, conversation: &models.Conversation{},
persistence: true, persistence: true,
stopSignal: make(chan interface{}),
replyChan: make(chan models.Message), replyChan: make(chan models.Message),
replyChunkChan: make(chan string), replyChunkChan: make(chan string),
} }
@ -448,6 +427,10 @@ func (m *model) handleInputKey(msg tea.KeyMsg) tea.Cmd {
return nil return nil
} }
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"))
}
reply := models.Message{ reply := models.Message{
Role: models.MessageRoleUser, Role: models.MessageRoleUser,
Content: userInput, Content: userInput,
@ -482,8 +465,17 @@ func (m *model) handleInputKey(msg tea.KeyMsg) tea.Cmd {
m.updateContent() m.updateContent()
m.content.GotoBottom() m.content.GotoBottom()
m.waitingForReply = true return m.promptLLM()
m.status = "Press ctrl+c to cancel" case "ctrl+r":
if len(m.messages) == 0 {
return nil
}
// TODO: retry from selected message
if m.messages[len(m.messages)-1].Role != models.MessageRoleUser {
m.messages = m.messages[:len(m.messages)-1]
m.updateContent()
}
m.content.GotoBottom()
return m.promptLLM() return m.promptLLM()
} }
return nil return nil
@ -538,6 +530,9 @@ func (m *model) generateConversationTitle() tea.Cmd {
} }
func (m *model) promptLLM() tea.Cmd { func (m *model) promptLLM() tea.Cmd {
m.waitingForReply = true
m.status = "Press ctrl+c to cancel"
return func() tea.Msg { return func() tea.Msg {
completionProvider, err := m.ctx.GetCompletionProvider(*m.ctx.Config.Defaults.Model) completionProvider, err := m.ctx.GetCompletionProvider(*m.ctx.Config.Defaults.Model)
if err != nil { if err != nil {
@ -555,16 +550,25 @@ func (m *model) promptLLM() tea.Cmd {
m.replyChan <- msg m.replyChan <- msg
} }
ctx, replyCancelFunc := context.WithCancel(context.Background()) ctx, cancel := context.WithCancel(context.Background())
m.replyCancelFunc = replyCancelFunc
canceled := false
go func() {
select {
case <-m.stopSignal:
canceled = true
cancel()
}
}()
resp, err := completionProvider.CreateChatCompletionStream( resp, err := completionProvider.CreateChatCompletionStream(
ctx, requestParams, m.messages, replyHandler, m.replyChunkChan, ctx, requestParams, m.messages, replyHandler, m.replyChunkChan,
) )
if err != nil { if err != nil && !canceled {
return msgResponseError(err) return msgResponseError(err)
} }
return msgResponseEnd(resp) return msgResponseEnd(resp)
} }
} }
@ -632,8 +636,7 @@ func (m *model) updateContent() {
case models.MessageRoleAssistant: case models.MessageRoleAssistant:
icon = "" icon = ""
style = assistantStyle style = assistantStyle
case models.MessageRoleToolCall: case models.MessageRoleToolCall, models.MessageRoleToolResult:
case models.MessageRoleToolResult:
icon = "🔧" icon = "🔧"
} }