@@ -1,12 +1,16 @@
1package sidebar
1package sidebar
8 tea "github.com/charmbracelet/bubbletea/v2"
9 tea "github.com/charmbracelet/bubbletea/v2"
9 "github.com/charmbracelet/crush/internal/config"
10 "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"
10 "github.com/charmbracelet/crush/internal/llm/models"
14 "github.com/charmbracelet/crush/internal/llm/models"
11 "github.com/charmbracelet/crush/internal/logging"
15 "github.com/charmbracelet/crush/internal/logging"
12 "github.com/charmbracelet/crush/internal/lsp"
16 "github.com/charmbracelet/crush/internal/lsp"
@@ -21,12 +25,22 @@ import (
21 "github.com/charmbracelet/crush/internal/tui/util"
25 "github.com/charmbracelet/crush/internal/tui/util"
22 "github.com/charmbracelet/crush/internal/version"
26 "github.com/charmbracelet/crush/internal/version"
23 "github.com/charmbracelet/lipgloss/v2"
27 "github.com/charmbracelet/lipgloss/v2"
28+ "github.com/charmbracelet/x/ansi"
27 logoBreakpoint = 65
32 logoBreakpoint = 65
35+type SessionFile struct {
40+type SessionFilesMsg struct {
30type Sidebar interface {
44type Sidebar interface {
31 util.Model
45 util.Model
32 layout.Sizeable
46 layout.Sizeable
@@ -38,11 +52,14 @@ type sidebarCmp struct {
38 logo string
52 logo string
39 cwd string
53 cwd string
40 lspClients map[string]*lsp.Client
54 lspClients map[string]*lsp.Client
55+ history history.Service
43−func NewSidebarCmp(lspClients map[string]*lsp.Client) Sidebar {
59+func NewSidebarCmp(history history.Service, lspClients map[string]*lsp.Client) Sidebar {
44 return &sidebarCmp{
60 return &sidebarCmp{
45 lspClients: lspClients,
61 lspClients: lspClients,
@@ -58,6 +75,12 @@ func (m *sidebarCmp) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
58 if msg.ID != m.session.ID {
75 if msg.ID != m.session.ID {
59 m.session = msg
76 m.session = msg
78+ return m, m.loadSessionFiles
79+ case SessionFilesMsg:
81+ logging.Info("Loaded session files", "count", len(m.files))
61 case chat.SessionClearedMsg:
84 case chat.SessionClearedMsg:
62 m.session = session.Session{}
85 m.session = session.Session{}
63 case pubsub.Event[session.Session]:
86 case pubsub.Event[session.Session]:
@@ -85,6 +108,8 @@ func (m *sidebarCmp) View() tea.View {
86 m.currentModelBlock(),
109 m.currentModelBlock(),
88 m.lspBlock(),
113 m.lspBlock(),
90 m.mcpBlock(),
115 m.mcpBlock(),
@@ -95,6 +120,58 @@ func (m *sidebarCmp) View() tea.View {
123+func (m *sidebarCmp) loadSessionFiles() tea.Msg {
124+ files, err := m.history.ListBySession(context.Background(), m.session.ID)
126+ return util.InfoMsg{
127+ Type: util.InfoTypeError,
132+ type fileHistory struct {
133+ initialVersion history.File
134+ latestVersion history.File
137+ fileMap := make(map[string]fileHistory)
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
145+ if file.Version == history.InitialVersion {
146+ existing.initialVersion = file
148+ fileMap[file.Path] = existing
150+ // Add the initial version
151+ fileMap[file.Path] = fileHistory{
152+ initialVersion: file,
153+ latestVersion: file,
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{
164+ Additions: additions,
165+ Deletions: deletions,
170+ return SessionFilesMsg{
171+ Files: sessionFiles,
98func (m *sidebarCmp) SetSize(width, height int) tea.Cmd {
175func (m *sidebarCmp) SetSize(width, height int) tea.Cmd {
99 if width < logoBreakpoint && m.width >= logoBreakpoint {
176 if width < logoBreakpoint && m.width >= logoBreakpoint {
100 m.logo = m.logoBlock(true)
177 m.logo = m.logoBlock(true)
@@ -122,6 +199,59 @@ func (m *sidebarCmp) logoBlock(compact bool) string {
202+func (m *sidebarCmp) filesBlock() string {
203+ maxWidth := min(m.width, 58)
204+ t := styles.CurrentTheme()
206+ section := t.S().Muted.Render(
207+ core.Section("Files", maxWidth),
210+ if len(m.files) == 0 {
211+ return lipgloss.JoinVertical(
215+ t.S().Base.Foreground(t.Border).Render("None"),
219+ fileList := []string{section, ""}
221+ for _, file := range m.files {
222+ // Extract just the filename from the path
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)))
229+ if file.Deletions > 0 {
230+ statusParts = append(statusParts, t.S().Base.Foreground(t.Error).Render(fmt.Sprintf("-%d", file.Deletions)))
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,
239+ IconColor: t.FgMuted,
242+ ExtraContent: extraContent,
249+ return lipgloss.JoinVertical(
125func (m *sidebarCmp) lspBlock() string {
255func (m *sidebarCmp) lspBlock() string {
126 maxWidth := min(m.width, 58)
256 maxWidth := min(m.width, 58)
127 t := styles.CurrentTheme()
257 t := styles.CurrentTheme()
@@ -177,7 +307,6 @@ func (m *sidebarCmp) lspBlock() string {
177 errs = append(errs, t.S().Base.Foreground(t.FgHalfMuted).Render(fmt.Sprintf("%s%d", styles.InfoIcon, lspErrs[protocol.SeverityInformation])))
307 errs = append(errs, t.S().Base.Foreground(t.FgHalfMuted).Render(fmt.Sprintf("%s%d", styles.InfoIcon, lspErrs[protocol.SeverityInformation])))
180− logging.Info("LSP Errors", "errors", errs)
181 lspList = append(lspList,
310 lspList = append(lspList,
182 core.Status(
311 core.Status(
183 core.StatusOpts{
312 core.StatusOpts{