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.Get().WorkingDir()), 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 agentCfg := config.Get().Agents["coder"]
115 model := config.Get().GetModelByType(agentCfg.Model)
116 percentage := (float64(h.session.CompletionTokens+h.session.PromptTokens) / float64(model.ContextWindow)) * 100
117 formattedPercentage := t.S().Muted.Render(fmt.Sprintf("%d%%", int(percentage)))
118 parts = append(parts, formattedPercentage)
119
120 if h.detailsOpen {
121 parts = append(parts, t.S().Muted.Render("ctrl+d")+t.S().Subtle.Render(" close"))
122 } else {
123 parts = append(parts, t.S().Muted.Render("ctrl+d")+t.S().Subtle.Render(" open "))
124 }
125 dot := t.S().Subtle.Render(" β’ ")
126 return strings.Join(parts, dot)
127}
128
129func (h *header) SetDetailsOpen(open bool) {
130 h.detailsOpen = open
131}
132
133// SetSession implements Header.
134func (h *header) SetSession(session session.Session) {
135 h.session = session
136}