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