Compare commits
5 Commits
cf46088762
...
e9fde37201
Author | SHA1 | Date | |
---|---|---|---|
e9fde37201 | |||
6242ea17d8 | |||
2ca94e1ffb | |||
2b0d474660 | |||
fdf8033aff |
@ -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 = "🔧"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user