header.go

  1package header
  2
  3import (
  4	"fmt"
  5	"strings"
  6
  7	tea "github.com/charmbracelet/bubbletea/v2"
  8	"github.com/charmbracelet/crush/internal/config"
  9	"github.com/charmbracelet/crush/internal/fsext"
 10	"github.com/charmbracelet/crush/internal/llm/models"
 11	"github.com/charmbracelet/crush/internal/lsp"
 12	"github.com/charmbracelet/crush/internal/lsp/protocol"
 13	"github.com/charmbracelet/crush/internal/pubsub"
 14	"github.com/charmbracelet/crush/internal/session"
 15	"github.com/charmbracelet/crush/internal/tui/components/chat"
 16	"github.com/charmbracelet/crush/internal/tui/styles"
 17	"github.com/charmbracelet/crush/internal/tui/util"
 18	"github.com/charmbracelet/lipgloss/v2"
 19)
 20
 21type Header interface {
 22	util.Model
 23	SetSession(session session.Session)
 24	SetDetailsOpen(open bool)
 25}
 26
 27type header struct {
 28	width       int
 29	session     session.Session
 30	lspClients  map[string]*lsp.Client
 31	detailsOpen bool
 32}
 33
 34func New(lspClients map[string]*lsp.Client) Header {
 35	return &header{
 36		lspClients: lspClients,
 37		width:      0,
 38	}
 39}
 40
 41func (h *header) Init() tea.Cmd {
 42	return nil
 43}
 44
 45func (p *header) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
 46	switch msg := msg.(type) {
 47	case tea.WindowSizeMsg:
 48		p.width = msg.Width - 2
 49	case chat.SessionSelectedMsg:
 50		p.session = msg
 51	case pubsub.Event[session.Session]:
 52		if msg.Type == pubsub.UpdatedEvent {
 53			if p.session.ID == msg.Payload.ID {
 54				p.session = msg.Payload
 55			}
 56		}
 57	}
 58	return p, nil
 59}
 60
 61func (p *header) View() tea.View {
 62	if p.session.ID == "" {
 63		return tea.NewView("")
 64	}
 65
 66	t := styles.CurrentTheme()
 67	details := p.details()
 68	parts := []string{
 69		t.S().Base.Foreground(t.Secondary).Render("Charmβ„’"),
 70		" ",
 71		styles.ApplyBoldForegroundGrad("CRUSH", t.Secondary, t.Primary),
 72		" ",
 73	}
 74
 75	remainingWidth := p.width - lipgloss.Width(strings.Join(parts, "")) - lipgloss.Width(details) - 2
 76	if remainingWidth > 0 {
 77		char := "β•±"
 78		lines := strings.Repeat(char, remainingWidth)
 79		parts = append(parts, t.S().Base.Foreground(t.Primary).Render(lines), " ")
 80	}
 81
 82	parts = append(parts, details)
 83
 84	content := t.S().Base.Padding(0, 1).Render(
 85		lipgloss.JoinHorizontal(
 86			lipgloss.Left,
 87			parts...,
 88		),
 89	)
 90	return tea.NewView(content)
 91}
 92
 93func (h *header) details() string {
 94	t := styles.CurrentTheme()
 95	cwd := fsext.DirTrim(fsext.PrettyPath(config.WorkingDirectory()), 4)
 96	parts := []string{
 97		t.S().Muted.Render(cwd),
 98	}
 99
100	errorCount := 0
101	for _, l := range h.lspClients {
102		for _, diagnostics := range l.GetDiagnostics() {
103			for _, diagnostic := range diagnostics {
104				if diagnostic.Severity == protocol.SeverityError {
105					errorCount++
106				}
107			}
108		}
109	}
110
111	if errorCount > 0 {
112		parts = append(parts, t.S().Error.Render(fmt.Sprintf("%s%d", styles.ErrorIcon, errorCount)))
113	}
114
115	cfg := config.Get()
116	agentCfg := cfg.Agents[config.AgentCoder]
117	selectedModelID := agentCfg.Model
118	model := models.SupportedModels[selectedModelID]
119
120	percentage := (float64(h.session.CompletionTokens+h.session.PromptTokens) / float64(model.ContextWindow)) * 100
121	formattedPercentage := t.S().Muted.Render(fmt.Sprintf("%d%%", int(percentage)))
122	parts = append(parts, formattedPercentage)
123
124	if h.detailsOpen {
125		parts = append(parts, t.S().Muted.Render("ctrl+d")+t.S().Subtle.Render(" close"))
126	} else {
127		parts = append(parts, t.S().Muted.Render("ctrl+d")+t.S().Subtle.Render(" open "))
128	}
129	dot := t.S().Subtle.Render(" β€’ ")
130	return strings.Join(parts, dot)
131}
132
133func (h *header) SetDetailsOpen(open bool) {
134	h.detailsOpen = open
135}
136
137// SetSession implements Header.
138func (h *header) SetSession(session session.Session) {
139	h.session = session
140}