@@ -1,12 +1,16 @@
11package sidebar
22
33import (
4+ "context"
45 "fmt"
56 "os"
67 "strings"
78
89 tea "github.com/charmbracelet/bubbletea/v2"
910 "github.com/charmbracelet/crush/internal/config"
11+ "github.com/charmbracelet/crush/internal/diff"
12+ "github.com/charmbracelet/crush/internal/fileutil"
13+ "github.com/charmbracelet/crush/internal/history"
1014 "github.com/charmbracelet/crush/internal/llm/models"
1115 "github.com/charmbracelet/crush/internal/logging"
1216 "github.com/charmbracelet/crush/internal/lsp"
@@ -21,12 +25,22 @@ import (
2125 "github.com/charmbracelet/crush/internal/tui/util"
2226 "github.com/charmbracelet/crush/internal/version"
2327 "github.com/charmbracelet/lipgloss/v2"
28+ "github.com/charmbracelet/x/ansi"
2429)
2530
2631const (
2732 logoBreakpoint = 65
2833)
2934
35+type SessionFile struct {
36+ FilePath string
37+ Additions int
38+ Deletions int
39+}
40+type SessionFilesMsg struct {
41+ Files []SessionFile
42+}
43+
3044type Sidebar interface {
3145 util.Model
3246 layout.Sizeable
@@ -38,11 +52,14 @@ type sidebarCmp struct {
3852 logo string
3953 cwd string
4054 lspClients map[string]*lsp.Client
55+ history history.Service
56+ files []SessionFile
4157}
4258
43−func NewSidebarCmp(lspClients map[string]*lsp.Client) Sidebar {
59+func NewSidebarCmp(history history.Service, lspClients map[string]*lsp.Client) Sidebar {
4460 return &sidebarCmp{
4561 lspClients: lspClients,
62+ history: history,
4663 }
4764}
4865
@@ -58,6 +75,12 @@ func (m *sidebarCmp) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
5875 if msg.ID != m.session.ID {
5976 m.session = msg
6077 }
78+ return m, m.loadSessionFiles
79+ case SessionFilesMsg:
80+ m.files = msg.Files
81+ logging.Info("Loaded session files", "count", len(m.files))
82+ return m, nil
83+
6184 case chat.SessionClearedMsg:
6285 m.session = session.Session{}
6386 case pubsub.Event[session.Session]:
@@ -85,6 +108,8 @@ func (m *sidebarCmp) View() tea.View {
85108 "",
86109 m.currentModelBlock(),
87110 "",
111+ m.filesBlock(),
112+ "",
88113 m.lspBlock(),
89114 "",
90115 m.mcpBlock(),
@@ -95,6 +120,58 @@ func (m *sidebarCmp) View() tea.View {
95120 )
96121}
97122
123+func (m *sidebarCmp) loadSessionFiles() tea.Msg {
124+ files, err := m.history.ListBySession(context.Background(), m.session.ID)
125+ if err != nil {
126+ return util.InfoMsg{
127+ Type: util.InfoTypeError,
128+ Msg: err.Error(),
129+ }
130+ }
131+
132+ type fileHistory struct {
133+ initialVersion history.File
134+ latestVersion history.File
135+ }
136+
137+ fileMap := make(map[string]fileHistory)
138+
139+ for _, file := range files {
140+ if existing, ok := fileMap[file.Path]; ok {
141+ // Update the latest version
142+ if existing.latestVersion.CreatedAt < file.CreatedAt {
143+ existing.latestVersion = file
144+ }
145+ if file.Version == history.InitialVersion {
146+ existing.initialVersion = file
147+ }
148+ fileMap[file.Path] = existing
149+ } else {
150+ // Add the initial version
151+ fileMap[file.Path] = fileHistory{
152+ initialVersion: file,
153+ latestVersion: file,
154+ }
155+ }
156+ }
157+
158+ sessionFiles := make([]SessionFile, 0, len(fileMap))
159+ for path, fh := range fileMap {
160+ if fh.initialVersion.Version == history.InitialVersion {
161+ _, additions, deletions := diff.GenerateDiff(fh.initialVersion.Content, fh.latestVersion.Content, fh.initialVersion.Path)
162+ sessionFiles = append(sessionFiles, SessionFile{
163+ FilePath: path,
164+ Additions: additions,
165+ Deletions: deletions,
166+ })
167+ }
168+ }
169+
170+ return SessionFilesMsg{
171+ Files: sessionFiles,
172+ }
173+}
174+
98175func (m *sidebarCmp) SetSize(width, height int) tea.Cmd {
99176 if width < logoBreakpoint && m.width >= logoBreakpoint {
100177 m.logo = m.logoBlock(true)
@@ -122,6 +199,59 @@ func (m *sidebarCmp) logoBlock(compact bool) string {
122199 })
123200}
124201
202+func (m *sidebarCmp) filesBlock() string {
203+ maxWidth := min(m.width, 58)
204+ t := styles.CurrentTheme()
205+
206+ section := t.S().Muted.Render(
207+ core.Section("Files", maxWidth),
208+ )
209+
210+ if len(m.files) == 0 {
211+ return lipgloss.JoinVertical(
212+ lipgloss.Left,
213+ section,
214+ "",
215+ t.S().Base.Foreground(t.Border).Render("None"),
216+ )
217+ }
218+
219+ fileList := []string{section, ""}
220+
221+ for _, file := range m.files {
222+ // Extract just the filename from the path
223+
224+ // Create status indicators for additions/deletions
225+ var statusParts []string
226+ if file.Additions > 0 {
227+ statusParts = append(statusParts, t.S().Base.Foreground(t.Success).Render(fmt.Sprintf("+%d", file.Additions)))
228+ }
229+ if file.Deletions > 0 {
230+ statusParts = append(statusParts, t.S().Base.Foreground(t.Error).Render(fmt.Sprintf("-%d", file.Deletions)))
231+ }
232+
233+ extraContent := strings.Join(statusParts, " ")
234+ filePath := fileutil.DirTrim(fileutil.PrettyPath(file.FilePath), 2)
235+ filePath = ansi.Truncate(filePath, maxWidth-lipgloss.Width(extraContent)-2, "…")
236+ fileList = append(fileList,
237+ core.Status(
238+ core.StatusOpts{
239+ IconColor: t.FgMuted,
240+ NoIcon: true,
241+ Title: filePath,
242+ ExtraContent: extraContent,
243+ },
244+ m.width,
245+ ),
246+ )
247+ }
248+
249+ return lipgloss.JoinVertical(
250+ lipgloss.Left,
251+ fileList...,
252+ )
253+}
254+
125255func (m *sidebarCmp) lspBlock() string {
126256 maxWidth := min(m.width, 58)
127257 t := styles.CurrentTheme()
@@ -177,7 +307,6 @@ func (m *sidebarCmp) lspBlock() string {
177307 errs = append(errs, t.S().Base.Foreground(t.FgHalfMuted).Render(fmt.Sprintf("%s%d", styles.InfoIcon, lspErrs[protocol.SeverityInformation])))
178308 }
179309
180− logging.Info("LSP Errors", "errors", errs)
181310 lspList = append(lspList,
182311 core.Status(
183312 core.StatusOpts{