|
|
|
|
@@ -41,7 +41,7 @@ var (
|
|
|
|
|
Faint(true).
|
|
|
|
|
Border(lipgloss.RoundedBorder(), true, true, true, false)
|
|
|
|
|
|
|
|
|
|
footerStyle = lipgloss.NewStyle()
|
|
|
|
|
footerStyle = lipgloss.NewStyle().Padding(0, 1)
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
func (m *Model) renderMessageHeading(i int, message *api.Message) string {
|
|
|
|
|
@@ -232,7 +232,7 @@ func (m *Model) conversationMessagesView() string {
|
|
|
|
|
if m.state == pendingResponse && m.App.Messages[len(m.App.Messages)-1].Role != api.MessageRoleAssistant {
|
|
|
|
|
heading := m.renderMessageHeading(-1, &api.Message{
|
|
|
|
|
Role: api.MessageRoleAssistant,
|
|
|
|
|
Metadata: api.MessageMeta {
|
|
|
|
|
Metadata: api.MessageMeta{
|
|
|
|
|
GenerationModel: &m.App.Model,
|
|
|
|
|
},
|
|
|
|
|
})
|
|
|
|
|
@@ -265,105 +265,99 @@ func (m *Model) Header(width int) string {
|
|
|
|
|
} else {
|
|
|
|
|
title = "Untitled"
|
|
|
|
|
}
|
|
|
|
|
title = tuiutil.TruncateToCellWidth(title, width-styles.Header.GetHorizontalPadding(), "...")
|
|
|
|
|
title = tuiutil.TruncateRightToCellWidth(title, width-styles.Header.GetHorizontalPadding(), "...")
|
|
|
|
|
header := titleStyle.Render(title)
|
|
|
|
|
return styles.Header.Width(width).Render(header)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (m *Model) Footer(width int) string {
|
|
|
|
|
segmentStyle := lipgloss.NewStyle().PaddingLeft(1).PaddingRight(1).Faint(true)
|
|
|
|
|
segmentSeparator := "|"
|
|
|
|
|
segmentStyle := lipgloss.NewStyle().Faint(true)
|
|
|
|
|
segmentSeparator := segmentStyle.Render(" | ")
|
|
|
|
|
|
|
|
|
|
// Left segments
|
|
|
|
|
leftSegments := make([]string, 0, 4)
|
|
|
|
|
|
|
|
|
|
leftSegments := []string{}
|
|
|
|
|
if m.state == pendingResponse {
|
|
|
|
|
leftSegments = append(leftSegments, segmentStyle.Render(m.spinner.View()))
|
|
|
|
|
} else {
|
|
|
|
|
leftSegments = append(leftSegments, segmentStyle.Render("∙∙∙"))
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if m.elapsed > 0 && m.tokenCount > 0 {
|
|
|
|
|
throughput := fmt.Sprintf("%.0f t/sec", float64(m.tokenCount)/m.elapsed.Seconds())
|
|
|
|
|
leftSegments = append(leftSegments, segmentStyle.Render(throughput))
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// var status string
|
|
|
|
|
// switch m.state {
|
|
|
|
|
// case pendingResponse:
|
|
|
|
|
// status = "Press ctrl+c to cancel"
|
|
|
|
|
// default:
|
|
|
|
|
// status = "Press ctrl+s to send"
|
|
|
|
|
// }
|
|
|
|
|
// leftSegments = append(leftSegments, segmentStyle.Render(status))
|
|
|
|
|
|
|
|
|
|
// Right segments
|
|
|
|
|
rightSegments := make([]string, 0, 8)
|
|
|
|
|
|
|
|
|
|
if m.App.Agent != nil {
|
|
|
|
|
rightSegments = append(rightSegments, segmentStyle.Render(m.App.Agent.Name))
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
model := segmentStyle.Render(m.App.ActiveModel(lipgloss.NewStyle()))
|
|
|
|
|
rightSegments = append(rightSegments, model)
|
|
|
|
|
|
|
|
|
|
savingStyle := segmentStyle.Bold(true)
|
|
|
|
|
saving := ""
|
|
|
|
|
if m.persistence {
|
|
|
|
|
saving = savingStyle.Foreground(lipgloss.Color("2")).Render("✅💾")
|
|
|
|
|
saving = savingStyle.Foreground(lipgloss.Color("2")).Render("💾✅")
|
|
|
|
|
} else {
|
|
|
|
|
saving = savingStyle.Foreground(lipgloss.Color("1")).Render("❌💾")
|
|
|
|
|
saving = savingStyle.Foreground(lipgloss.Color("1")).Render("💾❌")
|
|
|
|
|
}
|
|
|
|
|
leftSegments = append(leftSegments, saving)
|
|
|
|
|
rightSegments = append(rightSegments, saving)
|
|
|
|
|
|
|
|
|
|
// Right segments
|
|
|
|
|
|
|
|
|
|
rightSegments := []string{}
|
|
|
|
|
|
|
|
|
|
if m.elapsed > 0 && m.tokenCount > 0 {
|
|
|
|
|
throughput := fmt.Sprintf("%.0f t/sec", float64(m.tokenCount)/m.elapsed.Seconds())
|
|
|
|
|
rightSegments = append(rightSegments, segmentStyle.Render(throughput))
|
|
|
|
|
}
|
|
|
|
|
model := segmentStyle.Render(m.App.ActiveModel(lipgloss.NewStyle()))
|
|
|
|
|
rightSegments = append(rightSegments, model)
|
|
|
|
|
|
|
|
|
|
// Status
|
|
|
|
|
|
|
|
|
|
var status string
|
|
|
|
|
switch m.state {
|
|
|
|
|
case pendingResponse:
|
|
|
|
|
status = "Press ctrl+c to cancel" + m.spinner.View()
|
|
|
|
|
default:
|
|
|
|
|
status = "Press ctrl+s to send"
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return m.layoutFooter(width, leftSegments, status, rightSegments, segmentStyle, segmentSeparator)
|
|
|
|
|
return m.layoutFooter(width, leftSegments, rightSegments, segmentSeparator)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (m *Model) layoutFooter(
|
|
|
|
|
width int,
|
|
|
|
|
leftSegments []string,
|
|
|
|
|
status string,
|
|
|
|
|
rightSegments []string,
|
|
|
|
|
segmentStyle lipgloss.Style,
|
|
|
|
|
segmentSeparator string,
|
|
|
|
|
) string {
|
|
|
|
|
truncate := func(s string, w int) string {
|
|
|
|
|
return tuiutil.TruncateToCellWidth(s, w, "...")
|
|
|
|
|
}
|
|
|
|
|
padWidth := segmentStyle.GetHorizontalPadding()
|
|
|
|
|
|
|
|
|
|
left := strings.Join(leftSegments, segmentSeparator)
|
|
|
|
|
right := strings.Join(rightSegments, segmentSeparator)
|
|
|
|
|
|
|
|
|
|
leftWidth := tuiutil.Width(left)
|
|
|
|
|
rightWidth := tuiutil.Width(right)
|
|
|
|
|
sepWidth := tuiutil.Width(segmentSeparator)
|
|
|
|
|
frameWidth := footerStyle.GetHorizontalFrameSize()
|
|
|
|
|
|
|
|
|
|
availableWidth := width - leftWidth - rightWidth - tuiutil.Width(segmentSeparator)
|
|
|
|
|
availableWidth := width - frameWidth - leftWidth - rightWidth
|
|
|
|
|
|
|
|
|
|
statusWidth := tuiutil.Width(status)
|
|
|
|
|
if availableWidth >= statusWidth+padWidth {
|
|
|
|
|
if availableWidth >= sepWidth {
|
|
|
|
|
// Everything fits
|
|
|
|
|
availableWidth -= statusWidth + padWidth
|
|
|
|
|
padding := ""
|
|
|
|
|
if availableWidth > 0 {
|
|
|
|
|
padding = strings.Repeat(" ", availableWidth)
|
|
|
|
|
}
|
|
|
|
|
return footerStyle.Render(left + segmentSeparator + segmentStyle.Render(status) + padding + right)
|
|
|
|
|
padding := strings.Repeat(" ", availableWidth)
|
|
|
|
|
return footerStyle.Render(left + padding + right)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if availableWidth > 4 {
|
|
|
|
|
// There is some space left for a truncated status
|
|
|
|
|
truncatedStatus := truncate(status, availableWidth-padWidth)
|
|
|
|
|
return footerStyle.Width(width).Render(left + segmentSeparator + segmentStyle.Render(truncatedStatus) + right)
|
|
|
|
|
// Insert between left and right segments when they're being truncated
|
|
|
|
|
truncSeparator := "..."
|
|
|
|
|
|
|
|
|
|
totalAvailableWidth := width - frameWidth - len(truncSeparator)
|
|
|
|
|
|
|
|
|
|
minVisibleLength := 3
|
|
|
|
|
if totalAvailableWidth < 2*minVisibleLength {
|
|
|
|
|
minVisibleLength = totalAvailableWidth / 2
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if availableWidth >= 0 {
|
|
|
|
|
// Draw some dots...
|
|
|
|
|
dots := ""
|
|
|
|
|
if availableWidth == 1 {
|
|
|
|
|
dots = " "
|
|
|
|
|
} else if availableWidth > 1 {
|
|
|
|
|
dots = " " + strings.Repeat(".", availableWidth-1)
|
|
|
|
|
dots = lipgloss.NewStyle().Faint(true).Render(dots)
|
|
|
|
|
}
|
|
|
|
|
leftProportion := float64(leftWidth) / float64(leftWidth+rightWidth)
|
|
|
|
|
|
|
|
|
|
return footerStyle.Width(width).Render(left + segmentSeparator + dots + right)
|
|
|
|
|
}
|
|
|
|
|
newLeftWidth := int(max(float64(minVisibleLength), leftProportion*float64(totalAvailableWidth)))
|
|
|
|
|
newRightWidth := totalAvailableWidth - newLeftWidth
|
|
|
|
|
|
|
|
|
|
// Trucate right segment so it fits
|
|
|
|
|
right = truncate(right, tuiutil.Width(right)+availableWidth-1)
|
|
|
|
|
padding := ""
|
|
|
|
|
return footerStyle.Width(width).Render(left + segmentSeparator + padding + right)
|
|
|
|
|
truncatedLeft := faintStyle.Render(tuiutil.TruncateRightToCellWidth(left, newLeftWidth, ""))
|
|
|
|
|
truncatedRight := faintStyle.Render(tuiutil.TruncateLeftToCellWidth(right, newRightWidth, "..."))
|
|
|
|
|
|
|
|
|
|
return footerStyle.Width(width).Render(truncatedLeft + truncatedRight)
|
|
|
|
|
}
|
|
|
|
|
|