1package sidebar
2
3import (
4 "os"
5 "strings"
6
7 tea "github.com/charmbracelet/bubbletea/v2"
8 "github.com/charmbracelet/lipgloss/v2"
9 "github.com/opencode-ai/opencode/internal/config"
10 "github.com/opencode-ai/opencode/internal/pubsub"
11 "github.com/opencode-ai/opencode/internal/session"
12 "github.com/opencode-ai/opencode/internal/tui/components/chat"
13 "github.com/opencode-ai/opencode/internal/tui/components/core"
14 "github.com/opencode-ai/opencode/internal/tui/components/logo"
15 "github.com/opencode-ai/opencode/internal/tui/layout"
16 "github.com/opencode-ai/opencode/internal/tui/styles"
17 "github.com/opencode-ai/opencode/internal/tui/util"
18 "github.com/opencode-ai/opencode/internal/version"
19)
20
21const (
22 logoBreakpoint = 65
23)
24
25type Sidebar interface {
26 util.Model
27 layout.Sizeable
28}
29
30type sidebarCmp struct {
31 width, height int
32 session session.Session
33 logo string
34 cwd string
35}
36
37func NewSidebarCmp() Sidebar {
38 return &sidebarCmp{}
39}
40
41func (m *sidebarCmp) Init() tea.Cmd {
42 m.logo = m.logoBlock(false)
43 m.cwd = cwd()
44 return nil
45}
46
47func (m *sidebarCmp) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
48 switch msg := msg.(type) {
49 case chat.SessionSelectedMsg:
50 if msg.ID != m.session.ID {
51 m.session = msg
52 }
53 case pubsub.Event[session.Session]:
54 if msg.Type == pubsub.UpdatedEvent {
55 if m.session.ID == msg.Payload.ID {
56 m.session = msg.Payload
57 }
58 }
59 }
60 return m, nil
61}
62
63func (m *sidebarCmp) View() tea.View {
64 t := styles.CurrentTheme()
65 parts := []string{
66 m.logo,
67 }
68
69 if m.session.ID != "" {
70 parts = append(parts, t.S().Muted.Render(m.session.Title), "")
71 }
72
73 parts = append(parts,
74 m.cwd,
75 "",
76 m.lspBlock(),
77 "",
78 m.mcpBlock(),
79 )
80
81 return tea.NewView(
82 lipgloss.JoinVertical(lipgloss.Left, parts...),
83 )
84}
85
86func (m *sidebarCmp) SetSize(width, height int) tea.Cmd {
87 if width < logoBreakpoint && m.width >= logoBreakpoint {
88 m.logo = m.logoBlock(true)
89 } else if width >= logoBreakpoint && m.width < logoBreakpoint {
90 m.logo = m.logoBlock(false)
91 }
92
93 m.width = width
94 m.height = height
95 return nil
96}
97
98func (m *sidebarCmp) GetSize() (int, int) {
99 return m.width, m.height
100}
101
102func (m *sidebarCmp) logoBlock(compact bool) string {
103 t := styles.CurrentTheme()
104 return logo.Render(version.Version, compact, logo.Opts{
105 FieldColor: t.Primary,
106 TitleColorA: t.Secondary,
107 TitleColorB: t.Primary,
108 CharmColor: t.Secondary,
109 VersionColor: t.Primary,
110 })
111}
112
113func (m *sidebarCmp) lspBlock() string {
114 maxWidth := min(m.width, 58)
115 t := styles.CurrentTheme()
116
117 section := t.S().Muted.Render(
118 core.Section("LSPs", maxWidth),
119 )
120
121 lspList := []string{section, ""}
122
123 lsp := config.Get().LSP
124 if len(lsp) == 0 {
125 return lipgloss.JoinVertical(
126 lipgloss.Left,
127 section,
128 "",
129 t.S().Base.Foreground(t.Border).Render("None"),
130 )
131 }
132
133 for n, l := range lsp {
134 iconColor := t.Success
135 if l.Disabled {
136 iconColor = t.FgMuted
137 }
138 lspList = append(lspList,
139 core.Status(
140 core.StatusOpts{
141 IconColor: iconColor,
142 Title: n,
143 Description: l.Command,
144 },
145 m.width,
146 ),
147 )
148 }
149
150 return lipgloss.JoinVertical(
151 lipgloss.Left,
152 lspList...,
153 )
154}
155
156func (m *sidebarCmp) mcpBlock() string {
157 maxWidth := min(m.width, 58)
158 t := styles.CurrentTheme()
159
160 section := t.S().Muted.Render(
161 core.Section("MCPs", maxWidth),
162 )
163
164 mcpList := []string{section, ""}
165
166 mcp := config.Get().MCPServers
167 if len(mcp) == 0 {
168 return lipgloss.JoinVertical(
169 lipgloss.Left,
170 section,
171 "",
172 t.S().Base.Foreground(t.Border).Render("None"),
173 )
174 }
175
176 for n, l := range mcp {
177 iconColor := t.Success
178 mcpList = append(mcpList,
179 core.Status(
180 core.StatusOpts{
181 IconColor: iconColor,
182 Title: n,
183 Description: l.Command,
184 },
185 m.width,
186 ),
187 )
188 }
189
190 return lipgloss.JoinVertical(
191 lipgloss.Left,
192 mcpList...,
193 )
194}
195
196func cwd() string {
197 cwd := config.WorkingDirectory()
198 t := styles.CurrentTheme()
199 // replace home directory with ~
200 homeDir, err := os.UserHomeDir()
201 if err == nil {
202 cwd = strings.ReplaceAll(cwd, homeDir, "~")
203 }
204 return t.S().Muted.Render(cwd)
205}