fix: realtime session file changes (#2134)

Kujtim Hoxha created

Change summary

internal/ui/model/session.go | 168 ++++++++++++++-----------------------
internal/ui/model/ui.go      |   7 +
2 files changed, 70 insertions(+), 105 deletions(-)

Detailed changes

internal/ui/model/session.go 🔗

@@ -43,63 +43,68 @@ func (m *UI) loadSession(sessionID string) tea.Cmd {
 	return func() tea.Msg {
 		session, err := m.com.App.Sessions.Get(context.Background(), sessionID)
 		if err != nil {
-			// TODO: better error handling
-			return util.ReportError(err)()
+			return util.ReportError(err)
 		}
 
-		files, err := m.com.App.History.ListBySession(context.Background(), sessionID)
+		sessionFiles, err := m.loadSessionFiles(sessionID)
 		if err != nil {
-			// TODO: better error handling
-			return util.ReportError(err)()
+			return util.ReportError(err)
 		}
 
-		filesByPath := make(map[string][]history.File)
-		for _, f := range files {
-			filesByPath[f.Path] = append(filesByPath[f.Path], f)
+		return loadSessionMsg{
+			session: &session,
+			files:   sessionFiles,
 		}
+	}
+}
 
-		sessionFiles := make([]SessionFile, 0, len(filesByPath))
-		for _, versions := range filesByPath {
-			if len(versions) == 0 {
-				continue
-			}
-
-			first := versions[0]
-			last := versions[0]
-			for _, v := range versions {
-				if v.Version < first.Version {
-					first = v
-				}
-				if v.Version > last.Version {
-					last = v
-				}
-			}
-
-			_, additions, deletions := diff.GenerateDiff(first.Content, last.Content, first.Path)
+func (m *UI) loadSessionFiles(sessionID string) ([]SessionFile, error) {
+	files, err := m.com.App.History.ListBySession(context.Background(), sessionID)
+	if err != nil {
+		return nil, err
+	}
 
-			sessionFiles = append(sessionFiles, SessionFile{
-				FirstVersion:  first,
-				LatestVersion: last,
-				Additions:     additions,
-				Deletions:     deletions,
-			})
+	filesByPath := make(map[string][]history.File)
+	for _, f := range files {
+		filesByPath[f.Path] = append(filesByPath[f.Path], f)
+	}
+	sessionFiles := make([]SessionFile, 0, len(filesByPath))
+	for _, versions := range filesByPath {
+		if len(versions) == 0 {
+			continue
 		}
 
-		slices.SortFunc(sessionFiles, func(a, b SessionFile) int {
-			if a.LatestVersion.UpdatedAt > b.LatestVersion.UpdatedAt {
-				return -1
+		first := versions[0]
+		last := versions[0]
+		for _, v := range versions {
+			if v.Version < first.Version {
+				first = v
 			}
-			if a.LatestVersion.UpdatedAt < b.LatestVersion.UpdatedAt {
-				return 1
+			if v.Version > last.Version {
+				last = v
 			}
-			return 0
+		}
+
+		_, additions, deletions := diff.GenerateDiff(first.Content, last.Content, first.Path)
+
+		sessionFiles = append(sessionFiles, SessionFile{
+			FirstVersion:  first,
+			LatestVersion: last,
+			Additions:     additions,
+			Deletions:     deletions,
 		})
+	}
 
-		return loadSessionMsg{
-			session: &session,
-			files:   sessionFiles,
+	slices.SortFunc(sessionFiles, func(a, b SessionFile) int {
+		if a.LatestVersion.UpdatedAt > b.LatestVersion.UpdatedAt {
+			return -1
 		}
-	}
+		if a.LatestVersion.UpdatedAt < b.LatestVersion.UpdatedAt {
+			return 1
+		}
+		return 0
+	})
+	return sessionFiles, nil
 }
 
 // handleFileEvent processes file change events and updates the session file
@@ -110,59 +115,14 @@ func (m *UI) handleFileEvent(file history.File) tea.Cmd {
 	}
 
 	return func() tea.Msg {
-		existingIdx := -1
-		for i, sf := range m.sessionFiles {
-			if sf.FirstVersion.Path == file.Path {
-				existingIdx = i
-				break
-			}
-		}
-
-		if existingIdx == -1 {
-			newFiles := make([]SessionFile, 0, len(m.sessionFiles)+1)
-			newFiles = append(newFiles, SessionFile{
-				FirstVersion:  file,
-				LatestVersion: file,
-				Additions:     0,
-				Deletions:     0,
-			})
-			newFiles = append(newFiles, m.sessionFiles...)
-
-			return loadSessionMsg{
-				session: m.session,
-				files:   newFiles,
-			}
-		}
-
-		updated := m.sessionFiles[existingIdx]
-
-		if file.Version < updated.FirstVersion.Version {
-			updated.FirstVersion = file
-		}
-
-		if file.Version > updated.LatestVersion.Version {
-			updated.LatestVersion = file
-		}
-
-		_, additions, deletions := diff.GenerateDiff(
-			updated.FirstVersion.Content,
-			updated.LatestVersion.Content,
-			updated.FirstVersion.Path,
-		)
-		updated.Additions = additions
-		updated.Deletions = deletions
-
-		newFiles := make([]SessionFile, 0, len(m.sessionFiles))
-		newFiles = append(newFiles, updated)
-		for i, sf := range m.sessionFiles {
-			if i != existingIdx {
-				newFiles = append(newFiles, sf)
-			}
+		sessionFiles, err := m.loadSessionFiles(m.session.ID)
+		// could not load session files
+		if err != nil {
+			return util.NewErrorMsg(err)
 		}
 
-		return loadSessionMsg{
-			session: m.session,
-			files:   newFiles,
+		return sessionFilesUpdatesMsg{
+			sessionFiles: sessionFiles,
 		}
 	}
 }
@@ -177,9 +137,15 @@ func (m *UI) filesInfo(cwd string, width, maxItems int, isSection bool) string {
 		title = common.Section(t, "Modified Files", width)
 	}
 	list := t.Subtle.Render("None")
-
-	if len(m.sessionFiles) > 0 {
-		list = fileList(t, cwd, m.sessionFiles, width, maxItems)
+	var filesWithChanges []SessionFile
+	for _, f := range m.sessionFiles {
+		if f.Additions == 0 && f.Deletions == 0 {
+			continue
+		}
+		filesWithChanges = append(filesWithChanges, f)
+	}
+	if len(filesWithChanges) > 0 {
+		list = fileList(t, cwd, filesWithChanges, width, maxItems)
 	}
 
 	return lipgloss.NewStyle().Width(width).Render(fmt.Sprintf("%s\n\n%s", title, list))
@@ -187,21 +153,13 @@ func (m *UI) filesInfo(cwd string, width, maxItems int, isSection bool) string {
 
 // fileList renders a list of files with their diff statistics, truncating to
 // maxItems and showing a "...and N more" message if needed.
-func fileList(t *styles.Styles, cwd string, files []SessionFile, width, maxItems int) string {
+func fileList(t *styles.Styles, cwd string, filesWithChanges []SessionFile, width, maxItems int) string {
 	if maxItems <= 0 {
 		return ""
 	}
 	var renderedFiles []string
 	filesShown := 0
 
-	var filesWithChanges []SessionFile
-	for _, f := range files {
-		if f.Additions == 0 && f.Deletions == 0 {
-			continue
-		}
-		filesWithChanges = append(filesWithChanges, f)
-	}
-
 	for _, f := range filesWithChanges {
 		// Skip files with no changes
 		if filesShown >= maxItems {

internal/ui/model/ui.go 🔗

@@ -109,6 +109,11 @@ type (
 
 	// copyChatHighlightMsg is sent to copy the current chat highlight to clipboard.
 	copyChatHighlightMsg struct{}
+
+	// sessionFilesUpdatesMsg is sent when the files for this session have been updated
+	sessionFilesUpdatesMsg struct {
+		sessionFiles []SessionFile
+	}
 )
 
 // UI represents the main user interface model.
@@ -409,6 +414,8 @@ func (m *UI) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
 		m.historyReset()
 		cmds = append(cmds, m.loadPromptHistory())
 		m.updateLayoutAndSize()
+	case sessionFilesUpdatesMsg:
+		m.sessionFiles = msg.sessionFiles
 
 	case sendMessageMsg:
 		cmds = append(cmds, m.sendMessage(msg.Content, msg.Attachments...))