@@ -4,7 +4,9 @@ import (
44 "context"
55 "fmt"
66 "os"
7+ "sort"
78 "strings"
9+ "sync"
810
911 tea "github.com/charmbracelet/bubbletea/v2"
1012 "github.com/charmbracelet/crush/internal/config"
@@ -32,7 +34,13 @@ const (
3234 logoBreakpoint = 65
3335)
3436
37+type FileHistory struct {
38+ initialVersion history.File
39+ latestVersion history.File
40+}
41+
3542type SessionFile struct {
43+ History FileHistory
3644 FilePath string
3745 Additions int
3846 Deletions int
@@ -53,7 +61,8 @@ type sidebarCmp struct {
5361 cwd string
5462 lspClients map[string]*lsp.Client
5563 history history.Service
56− files []SessionFile
64+ // Using a sync map here because we might receive file history events concurrently
65+ files sync.Map
5766}
5867
5968func NewSidebarCmp(history history.Service, lspClients map[string]*lsp.Client) Sidebar {
@@ -77,12 +86,17 @@ func (m *sidebarCmp) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
7786 }
7887 return m, m.loadSessionFiles
7988 case SessionFilesMsg:
80− m.files = msg.Files
81− logging.Info("Loaded session files", "count", len(m.files))
89+ m.files = sync.Map{}
90+ for _, file := range msg.Files {
91+ m.files.Store(file.FilePath, file)
92+ }
8293 return m, nil
8394
8495 case chat.SessionClearedMsg:
8596 m.session = session.Session{}
97+ case pubsub.Event[history.File]:
98+ logging.Info("sidebar", "Received file history event", "file", msg.Payload.Path, "session", msg.Payload.SessionID)
99+ return m, m.handleFileHistoryEvent(msg)
86100 case pubsub.Event[session.Session]:
87101 if msg.Type == pubsub.UpdatedEvent {
88102 if m.session.ID == msg.Payload.ID {
@@ -123,6 +137,50 @@ func (m *sidebarCmp) View() tea.View {
123137 )
124138}
125139
140+func (m *sidebarCmp) handleFileHistoryEvent(event pubsub.Event[history.File]) tea.Cmd {
141+ return func() tea.Msg {
142+ file := event.Payload
143+ found := false
144+ m.files.Range(func(key, value any) bool {
145+ existing := value.(SessionFile)
146+ if existing.FilePath == file.Path {
147+ if existing.History.latestVersion.Version < file.Version {
148+ existing.History.latestVersion = file
149+ } else if file.Version == 0 {
150+ existing.History.initialVersion = file
151+ } else {
152+ // If the version is not greater than the latest, we ignore it
153+ return true
154+ }
155+ before := existing.History.initialVersion.Content
156+ after := existing.History.latestVersion.Content
157+ path := existing.History.initialVersion.Path
158+ _, additions, deletions := diff.GenerateDiff(before, after, path)
159+ existing.Additions = additions
160+ existing.Deletions = deletions
161+ m.files.Store(file.Path, existing)
162+ found = true
163+ return false
164+ }
165+ return true
166+ })
167+ if found {
168+ return nil
169+ }
170+ sf := SessionFile{
171+ History: FileHistory{
172+ initialVersion: file,
173+ latestVersion: file,
174+ },
175+ FilePath: file.Path,
176+ Additions: 0,
177+ Deletions: 0,
178+ }
179+ m.files.Store(file.Path, sf)
180+ return nil
181+ }
182+}
183+
126184func (m *sidebarCmp) loadSessionFiles() tea.Msg {
127185 files, err := m.history.ListBySession(context.Background(), m.session.ID)
128186 if err != nil {
@@ -132,26 +190,16 @@ func (m *sidebarCmp) loadSessionFiles() tea.Msg {
132190 }
133191 }
134192
135− type fileHistory struct {
136− initialVersion history.File
137− latestVersion history.File
138− }
139−
140− fileMap := make(map[string]fileHistory)
193+ fileMap := make(map[string]FileHistory)
141194
142195 for _, file := range files {
143196 if existing, ok := fileMap[file.Path]; ok {
144197 // Update the latest version
145− if existing.latestVersion.CreatedAt < file.CreatedAt {
146− existing.latestVersion = file
147− }
148− if file.Version == history.InitialVersion {
149− existing.initialVersion = file
150− }
198+ existing.latestVersion = file
151199 fileMap[file.Path] = existing
152200 } else {
153201 // Add the initial version
154− fileMap[file.Path] = fileHistory{
202+ fileMap[file.Path] = FileHistory{
155203 initialVersion: file,
156204 latestVersion: file,
157205 }
@@ -160,14 +208,13 @@ func (m *sidebarCmp) loadSessionFiles() tea.Msg {
160208
161209 sessionFiles := make([]SessionFile, 0, len(fileMap))
162210 for path, fh := range fileMap {
163− if fh.initialVersion.Version == history.InitialVersion {
164− _, additions, deletions := diff.GenerateDiff(fh.initialVersion.Content, fh.latestVersion.Content, fh.initialVersion.Path)
165− sessionFiles = append(sessionFiles, SessionFile{
166− FilePath: path,
167− Additions: additions,
168− Deletions: deletions,
169− })
170− }
211+ _, additions, deletions := diff.GenerateDiff(fh.initialVersion.Content, fh.latestVersion.Content, fh.initialVersion.Path)
212+ sessionFiles = append(sessionFiles, SessionFile{
213+ History: fh,
214+ FilePath: path,
215+ Additions: additions,
216+ Deletions: deletions,
217+ })
171218 }
172219
173220 return SessionFilesMsg{
@@ -210,7 +257,13 @@ func (m *sidebarCmp) filesBlock() string {
210257 core.Section("Modified Files", maxWidth),
211258 )
212259
213− if len(m.files) == 0 {
260+ files := make([]SessionFile, 0)
261+ m.files.Range(func(key, value any) bool {
262+ file := value.(SessionFile)
263+ files = append(files, file)
264+ return true // continue iterating
265+ })
266+ if len(files) == 0 {
214267 return lipgloss.JoinVertical(
215268 lipgloss.Left,
216269 section,
@@ -220,8 +273,12 @@ func (m *sidebarCmp) filesBlock() string {
220273 }
221274
222275 fileList := []string{section, ""}
276+ // order files by the latest version's created time
277+ sort.Slice(files, func(i, j int) bool {
278+ return files[i].History.latestVersion.CreatedAt > files[j].History.latestVersion.CreatedAt
279+ })
223280
224− for _, file := range m.files {
281+ for _, file := range files {
225282 // Extract just the filename from the path
226283
227284 // Create status indicators for additions/deletions