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