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}