@@ -4,7 +4,9 @@ import (
9 tea "github.com/charmbracelet/bubbletea/v2"
11 tea "github.com/charmbracelet/bubbletea/v2"
10 "github.com/charmbracelet/crush/internal/config"
12 "github.com/charmbracelet/crush/internal/config"
@@ -32,7 +34,13 @@ const (
32 logoBreakpoint = 65
34 logoBreakpoint = 65
37+type FileHistory struct {
38+ initialVersion history.File
39+ latestVersion history.File
35type SessionFile struct {
42type SessionFile struct {
36 FilePath string
44 FilePath string
37 Additions int
45 Additions int
38 Deletions int
46 Deletions int
@@ -53,7 +61,8 @@ type sidebarCmp struct {
53 cwd string
61 cwd string
54 lspClients map[string]*lsp.Client
62 lspClients map[string]*lsp.Client
55 history history.Service
63 history history.Service
56− files []SessionFile
64+ // Using a sync map here because we might receive file history events concurrently
59func NewSidebarCmp(history history.Service, lspClients map[string]*lsp.Client) Sidebar {
68func 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) {
78 return m, m.loadSessionFiles
87 return m, m.loadSessionFiles
79 case SessionFilesMsg:
88 case SessionFilesMsg:
80− m.files = msg.Files
90+ for _, file := range msg.Files {
81− logging.Info("Loaded session files", "count", len(m.files))
91+ m.files.Store(file.FilePath, file)
82 return m, nil
93 return m, nil
84 case chat.SessionClearedMsg:
95 case chat.SessionClearedMsg:
85 m.session = session.Session{}
96 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)
86 case pubsub.Event[session.Session]:
100 case pubsub.Event[session.Session]:
87 if msg.Type == pubsub.UpdatedEvent {
101 if msg.Type == pubsub.UpdatedEvent {
88 if m.session.ID == msg.Payload.ID {
102 if m.session.ID == msg.Payload.ID {
@@ -123,6 +137,50 @@ func (m *sidebarCmp) View() tea.View {
140+func (m *sidebarCmp) handleFileHistoryEvent(event pubsub.Event[history.File]) tea.Cmd {
141+ return func() tea.Msg {
142+ file := event.Payload
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
152+ // If the version is not greater than the latest, we ignore it
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)
171+ History: FileHistory{
172+ initialVersion: file,
173+ latestVersion: file,
175+ FilePath: file.Path,
179+ m.files.Store(file.Path, sf)
126func (m *sidebarCmp) loadSessionFiles() tea.Msg {
184func (m *sidebarCmp) loadSessionFiles() tea.Msg {
127 files, err := m.history.ListBySession(context.Background(), m.session.ID)
185 files, err := m.history.ListBySession(context.Background(), m.session.ID)
128 if err != nil {
186 if err != nil {
@@ -132,26 +190,16 @@ func (m *sidebarCmp) loadSessionFiles() tea.Msg {
135− type fileHistory struct {
136− initialVersion history.File
137− latestVersion history.File
140− fileMap := make(map[string]fileHistory)
193+ fileMap := make(map[string]FileHistory)
142 for _, file := range files {
195 for _, file := range files {
143 if existing, ok := fileMap[file.Path]; ok {
196 if existing, ok := fileMap[file.Path]; ok {
144 // Update the latest version
197 // Update the latest version
145− if existing.latestVersion.CreatedAt < file.CreatedAt {
146− existing.latestVersion = file
198+ existing.latestVersion = file
148− if file.Version == history.InitialVersion {
149− existing.initialVersion = file
151 fileMap[file.Path] = existing
199 fileMap[file.Path] = existing
153 // Add the initial version
201 // Add the initial version
154− fileMap[file.Path] = fileHistory{
202+ fileMap[file.Path] = FileHistory{
155 initialVersion: file,
203 initialVersion: file,
156 latestVersion: file,
204 latestVersion: file,
@@ -160,14 +208,13 @@ func (m *sidebarCmp) loadSessionFiles() tea.Msg {
161 sessionFiles := make([]SessionFile, 0, len(fileMap))
209 sessionFiles := make([]SessionFile, 0, len(fileMap))
162 for path, fh := range fileMap {
210 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)
211+ _, additions, deletions := diff.GenerateDiff(fh.initialVersion.Content, fh.latestVersion.Content, fh.initialVersion.Path)
165− sessionFiles = append(sessionFiles, SessionFile{
212+ sessionFiles = append(sessionFiles, SessionFile{
166− FilePath: path,
214+ FilePath: path,
167− Additions: additions,
215+ Additions: additions,
168− Deletions: deletions,
216+ Deletions: deletions,
173 return SessionFilesMsg{
220 return SessionFilesMsg{
@@ -210,7 +257,13 @@ func (m *sidebarCmp) filesBlock() string {
210 core.Section("Modified Files", maxWidth),
257 core.Section("Modified Files", maxWidth),
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
213− if len(m.files) == 0 {
266+ if len(files) == 0 {
214 return lipgloss.JoinVertical(
267 return lipgloss.JoinVertical(
215 lipgloss.Left,
268 lipgloss.Left,
@@ -220,8 +273,12 @@ func (m *sidebarCmp) filesBlock() string {
222 fileList := []string{section, ""}
275 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
224− for _, file := range m.files {
281+ for _, file := range files {
225 // Extract just the filename from the path
282 // Extract just the filename from the path
227 // Create status indicators for additions/deletions
284 // Create status indicators for additions/deletions