on main help sign in
feat: add file changes
e8f07b0Kujtim Hoxha
internal/fileutil/fileutil.go+24
@@ -211,3 +211,27 @@ func PrettyPath(path string) string {
211 }
211 }
212 return path
212 return path
213}
213}
214+
215+func DirTrim(pwd string, lim int) string {
216+ var (
217+ out string
218+ sep = string(filepath.Separator)
219+ )
220+ dirs := strings.Split(pwd, sep)
221+ if lim > len(dirs)-1 || lim <= 0 {
222+ return pwd
223+ }
224+ for i := len(dirs) - 1; i > 0; i-- {
225+ out = sep + out
226+ if i == len(dirs)-1 {
227+ out = dirs[i]
228+ } else if i >= len(dirs)-lim {
229+ out = string(dirs[i][0]) + out
230+ } else {
231+ out = "..." + out
232+ break
233+ }
234+ }
235+ out = filepath.Join("~", out)
236+ return out
237+}
internal/tui/components/chat/sidebar/sidebar.go+131−2
@@ -1,12 +1,16 @@
1package sidebar
1package sidebar
2
2
3import (
3import (
4+ "context"
4 "fmt"
5 "fmt"
5 "os"
6 "os"
6 "strings"
7 "strings"
7
8
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"
24)
29)
25
30
26const (
31const (
27 logoBreakpoint = 65
32 logoBreakpoint = 65
28)
33)
29
34
35+type SessionFile struct {
36+ FilePath string
37+ Additions int
38+ Deletions int
39+}
40+type SessionFilesMsg struct {
41+ Files []SessionFile
42+}
43+
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
56+ files []SessionFile
41}
57}
42
58
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,
62+ history: history,
46 }
63 }
47}
64}
48
65
@@ -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
60 }
77 }
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+
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 {
85 "",
108 "",
86 m.currentModelBlock(),
109 m.currentModelBlock(),
87 "",
110 "",
111+ m.filesBlock(),
112+ "",
88 m.lspBlock(),
113 m.lspBlock(),
89 "",
114 "",
90 m.mcpBlock(),
115 m.mcpBlock(),
@@ -95,6 +120,58 @@ func (m *sidebarCmp) View() tea.View {
95 )
120 )
96}
121}
97
122
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+
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 {
122 })
199 })
123}
200}
124
201
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+
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])))
178 }
308 }
179
309
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{
internal/tui/components/core/helpers.go+8−5
@@ -38,6 +38,7 @@ func Title(title string, width int) string {
38type StatusOpts struct {
38type StatusOpts struct {
39 Icon string
39 Icon string
40 IconColor color.Color
40 IconColor color.Color
41+ NoIcon bool // If true, no icon will be displayed
41 Title string
42 Title string
42 TitleColor color.Color
43 TitleColor color.Color
43 Description string
44 Description string
@@ -51,6 +52,8 @@ func Status(ops StatusOpts, width int) string {
51 iconColor := t.Success
52 iconColor := t.Success
52 if ops.Icon != "" {
53 if ops.Icon != "" {
53 icon = ops.Icon
54 icon = ops.Icon
55+ } else if ops.NoIcon {
56+ icon = ""
54 }
57 }
55 if ops.IconColor != nil {
58 if ops.IconColor != nil {
56 iconColor = ops.IconColor
59 iconColor = ops.IconColor
@@ -65,7 +68,6 @@ func Status(ops StatusOpts, width int) string {
65 if ops.DescriptionColor != nil {
68 if ops.DescriptionColor != nil {
66 descriptionColor = ops.DescriptionColor
69 descriptionColor = ops.DescriptionColor
67 }
70 }
68− icon = t.S().Base.Foreground(iconColor).Render(icon)
69 title = t.S().Base.Foreground(titleColor).Render(title)
71 title = t.S().Base.Foreground(titleColor).Render(title)
70 if description != "" {
72 if description != "" {
71 extraContent := len(ops.ExtraContent)
73 extraContent := len(ops.ExtraContent)
@@ -75,11 +77,12 @@ func Status(ops StatusOpts, width int) string {
75 description = ansi.Truncate(description, width-lipgloss.Width(icon)-lipgloss.Width(title)-2-extraContent, "…")
77 description = ansi.Truncate(description, width-lipgloss.Width(icon)-lipgloss.Width(title)-2-extraContent, "…")
76 }
78 }
77 description = t.S().Base.Foreground(descriptionColor).Render(description)
79 description = t.S().Base.Foreground(descriptionColor).Render(description)
80+
78− content := []string{
81+ content := []string{}
79− icon,
82+ if icon != "" {
80− title,
83+ content = append(content, t.S().Base.Foreground(iconColor).Render(icon))
81− description,
82 }
84 }
85+ content = append(content, title, description)
83 if ops.ExtraContent != "" {
86 if ops.ExtraContent != "" {
84 content = append(content, ops.ExtraContent)
87 content = append(content, ops.ExtraContent)
85 }
88 }
internal/tui/page/chat/chat.go+1−1
@@ -209,7 +209,7 @@ func (p *chatPage) Bindings() []key.Binding {
209
209
210func NewChatPage(app *app.App) ChatPage {
210func NewChatPage(app *app.App) ChatPage {
211 sidebarContainer := layout.NewContainer(
211 sidebarContainer := layout.NewContainer(
212− sidebar.NewSidebarCmp(app.LSPClients),
212+ sidebar.NewSidebarCmp(app.History, app.LSPClients),
213 layout.WithPadding(1, 1, 1, 1),
213 layout.WithPadding(1, 1, 1, 1),
214 )
214 )
215 editorContainer := layout.NewContainer(
215 editorContainer := layout.NewContainer(
todos.md+1
@@ -27,6 +27,7 @@
27- [ ] Update interactive mode to use the spinner
27- [ ] Update interactive mode to use the spinner
28- [ ] Revisit the core list component
28- [ ] Revisit the core list component
29 - [ ] This component has become super complex we might need to fix this.
29 - [ ] This component has become super complex we might need to fix this.
30+- [ ] Handle correct LSP and MCP status icon
30- [ ] Investigate ways to make the spinner less CPU intensive
31- [ ] Investigate ways to make the spinner less CPU intensive
31- [ ] General cleanup and documentation
32- [ ] General cleanup and documentation
32- [ ] Update the readme
33- [ ] Update the readme