on main help sign in
fix: fix the file history table and implement realtime file updates
ac5fb91Kujtim Hoxha
cmd/root.go+1
@@ -256,6 +256,7 @@ func setupSubscriptions(app *app.App, parentCtx context.Context) (chan tea.Msg,
256 setupSubscriber(ctx, &wg, "messages", app.Messages.Subscribe, ch)
256 setupSubscriber(ctx, &wg, "messages", app.Messages.Subscribe, ch)
257 setupSubscriber(ctx, &wg, "permissions", app.Permissions.Subscribe, ch)
257 setupSubscriber(ctx, &wg, "permissions", app.Permissions.Subscribe, ch)
258 setupSubscriber(ctx, &wg, "coderAgent", app.CoderAgent.Subscribe, ch)
258 setupSubscriber(ctx, &wg, "coderAgent", app.CoderAgent.Subscribe, ch)
259+ setupSubscriber(ctx, &wg, "history", app.History.Subscribe, ch)
259
260
260 cleanupFunc := func() {
261 cleanupFunc := func() {
261 logging.Info("Cancelling all subscriptions")
262 logging.Info("Cancelling all subscriptions")
cspell.json+1−1
@@ -1 +1 @@
1−{"flagWords":[],"language":"en","words":["crush","charmbracelet","lipgloss","bubbletea","textinput","Focusable","lsps","Sourcegraph","filepicker","imageorient","rasterx","oksvg","termenv","trashhalo","lucasb","nfnt","srwiley","Lanczos"],"version":"0.2"}
1+{"version":"0.2","language":"en","flagWords":[],"words":["crush","charmbracelet","lipgloss","bubbletea","textinput","Focusable","lsps","Sourcegraph","filepicker","imageorient","rasterx","oksvg","termenv","trashhalo","lucasb","nfnt","srwiley","Lanczos","fsext"]}
internal/db/db.go−10
@@ -78,9 +78,6 @@ func Prepare(ctx context.Context, db DBTX) (*Queries, error) {
78 if q.listSessionsStmt, err = db.PrepareContext(ctx, listSessions); err != nil {
78 if q.listSessionsStmt, err = db.PrepareContext(ctx, listSessions); err != nil {
79 return nil, fmt.Errorf("error preparing query ListSessions: %w", err)
79 return nil, fmt.Errorf("error preparing query ListSessions: %w", err)
80 }
80 }
81− if q.updateFileStmt, err = db.PrepareContext(ctx, updateFile); err != nil {
82− return nil, fmt.Errorf("error preparing query UpdateFile: %w", err)
83− }
84 if q.updateMessageStmt, err = db.PrepareContext(ctx, updateMessage); err != nil {
81 if q.updateMessageStmt, err = db.PrepareContext(ctx, updateMessage); err != nil {
85 return nil, fmt.Errorf("error preparing query UpdateMessage: %w", err)
82 return nil, fmt.Errorf("error preparing query UpdateMessage: %w", err)
86 }
83 }
@@ -182,11 +179,6 @@ func (q *Queries) Close() error {
182 err = fmt.Errorf("error closing listSessionsStmt: %w", cerr)
179 err = fmt.Errorf("error closing listSessionsStmt: %w", cerr)
183 }
180 }
184 }
181 }
185− if q.updateFileStmt != nil {
186− if cerr := q.updateFileStmt.Close(); cerr != nil {
187− err = fmt.Errorf("error closing updateFileStmt: %w", cerr)
188− }
189− }
190 if q.updateMessageStmt != nil {
182 if q.updateMessageStmt != nil {
191 if cerr := q.updateMessageStmt.Close(); cerr != nil {
183 if cerr := q.updateMessageStmt.Close(); cerr != nil {
192 err = fmt.Errorf("error closing updateMessageStmt: %w", cerr)
184 err = fmt.Errorf("error closing updateMessageStmt: %w", cerr)
@@ -254,7 +246,6 @@ type Queries struct {
254 listMessagesBySessionStmt *sql.Stmt
246 listMessagesBySessionStmt *sql.Stmt
255 listNewFilesStmt *sql.Stmt
247 listNewFilesStmt *sql.Stmt
256 listSessionsStmt *sql.Stmt
248 listSessionsStmt *sql.Stmt
257− updateFileStmt *sql.Stmt
258 updateMessageStmt *sql.Stmt
249 updateMessageStmt *sql.Stmt
259 updateSessionStmt *sql.Stmt
250 updateSessionStmt *sql.Stmt
260}
251}
@@ -281,7 +272,6 @@ func (q *Queries) WithTx(tx *sql.Tx) *Queries {
281 listMessagesBySessionStmt: q.listMessagesBySessionStmt,
272 listMessagesBySessionStmt: q.listMessagesBySessionStmt,
282 listNewFilesStmt: q.listNewFilesStmt,
273 listNewFilesStmt: q.listNewFilesStmt,
283 listSessionsStmt: q.listSessionsStmt,
274 listSessionsStmt: q.listSessionsStmt,
284− updateFileStmt: q.updateFileStmt,
285 updateMessageStmt: q.updateMessageStmt,
275 updateMessageStmt: q.updateMessageStmt,
286 updateSessionStmt: q.updateSessionStmt,
276 updateSessionStmt: q.updateSessionStmt,
287 }
277 }
internal/db/files.sql.go+7−38
@@ -29,7 +29,7 @@ type CreateFileParams struct {
29 SessionID string `json:"session_id"`
29 SessionID string `json:"session_id"`
30 Path string `json:"path"`
30 Path string `json:"path"`
31 Content string `json:"content"`
31 Content string `json:"content"`
32− Version string `json:"version"`
32+ Version int64 `json:"version"`
33}
33}
34
34
35func (q *Queries) CreateFile(ctx context.Context, arg CreateFileParams) (File, error) {
35func (q *Queries) CreateFile(ctx context.Context, arg CreateFileParams) (File, error) {
@@ -98,7 +98,7 @@ const getFileByPathAndSession = `-- name: GetFileByPathAndSession :one
98SELECT id, session_id, path, content, version, created_at, updated_at
98SELECT id, session_id, path, content, version, created_at, updated_at
99FROM files
99FROM files
100WHERE path = ? AND session_id = ?
100WHERE path = ? AND session_id = ?
101−ORDER BY created_at DESC
101+ORDER BY version DESC, created_at DESC
102LIMIT 1
102LIMIT 1
103`
103`
104
104
@@ -126,7 +126,7 @@ const listFilesByPath = `-- name: ListFilesByPath :many
126SELECT id, session_id, path, content, version, created_at, updated_at
126SELECT id, session_id, path, content, version, created_at, updated_at
127FROM files
127FROM files
128WHERE path = ?
128WHERE path = ?
129−ORDER BY created_at DESC
129+ORDER BY version DESC, created_at DESC
130`
130`
131
131
132func (q *Queries) ListFilesByPath(ctx context.Context, path string) ([]File, error) {
132func (q *Queries) ListFilesByPath(ctx context.Context, path string) ([]File, error) {
@@ -164,7 +164,7 @@ const listFilesBySession = `-- name: ListFilesBySession :many
164SELECT id, session_id, path, content, version, created_at, updated_at
164SELECT id, session_id, path, content, version, created_at, updated_at
165FROM files
165FROM files
166WHERE session_id = ?
166WHERE session_id = ?
167−ORDER BY created_at ASC
167+ORDER BY version ASC, created_at ASC
168`
168`
169
169
170func (q *Queries) ListFilesBySession(ctx context.Context, sessionID string) ([]File, error) {
170func (q *Queries) ListFilesBySession(ctx context.Context, sessionID string) ([]File, error) {
@@ -202,10 +202,10 @@ const listLatestSessionFiles = `-- name: ListLatestSessionFiles :many
202SELECT f.id, f.session_id, f.path, f.content, f.version, f.created_at, f.updated_at
202SELECT f.id, f.session_id, f.path, f.content, f.version, f.created_at, f.updated_at
203FROM files f
203FROM files f
204INNER JOIN (
204INNER JOIN (
205− SELECT path, MAX(created_at) as max_created_at
205+ SELECT path, MAX(version) as max_version, MAX(created_at) as max_created_at
206 FROM files
206 FROM files
207 GROUP BY path
207 GROUP BY path
208−) latest ON f.path = latest.path AND f.created_at = latest.max_created_at
208+) latest ON f.path = latest.path AND f.version = latest.max_version AND f.created_at = latest.max_created_at
209WHERE f.session_id = ?
209WHERE f.session_id = ?
210ORDER BY f.path
210ORDER BY f.path
211`
211`
@@ -245,7 +245,7 @@ const listNewFiles = `-- name: ListNewFiles :many
245SELECT id, session_id, path, content, version, created_at, updated_at
245SELECT id, session_id, path, content, version, created_at, updated_at
246FROM files
246FROM files
247WHERE is_new = 1
247WHERE is_new = 1
248−ORDER BY created_at DESC
248+ORDER BY version DESC, created_at DESC
249`
249`
250
250
251func (q *Queries) ListNewFiles(ctx context.Context) ([]File, error) {
251func (q *Queries) ListNewFiles(ctx context.Context) ([]File, error) {
@@ -278,34 +278,3 @@ func (q *Queries) ListNewFiles(ctx context.Context) ([]File, error) {
278 }
278 }
279 return items, nil
279 return items, nil
280}
280}
281−
282−const updateFile = `-- name: UpdateFile :one
283−UPDATE files
284−SET
285− content = ?,
286− version = ?,
287− updated_at = strftime('%s', 'now')
288−WHERE id = ?
289−RETURNING id, session_id, path, content, version, created_at, updated_at
290−`
291−
292−type UpdateFileParams struct {
293− Content string `json:"content"`
294− Version string `json:"version"`
295− ID string `json:"id"`
296−}
297−
298−func (q *Queries) UpdateFile(ctx context.Context, arg UpdateFileParams) (File, error) {
299− row := q.queryRow(ctx, q.updateFileStmt, updateFile, arg.Content, arg.Version, arg.ID)
300− var i File
301− err := row.Scan(
302− &i.ID,
303− &i.SessionID,
304− &i.Path,
305− &i.Content,
306− &i.Version,
307− &i.CreatedAt,
308− &i.UpdatedAt,
309− )
310− return i, err
311−}
internal/db/migrations/20250424200609_initial.sql+1−1
@@ -26,7 +26,7 @@ CREATE TABLE IF NOT EXISTS files (
26 session_id TEXT NOT NULL,
26 session_id TEXT NOT NULL,
27 path TEXT NOT NULL,
27 path TEXT NOT NULL,
28 content TEXT NOT NULL,
28 content TEXT NOT NULL,
29− version TEXT NOT NULL,
29+ version INTEGER NOT NULL DEFAULT 0,
30 created_at INTEGER NOT NULL, -- Unix timestamp in milliseconds
30 created_at INTEGER NOT NULL, -- Unix timestamp in milliseconds
31 updated_at INTEGER NOT NULL, -- Unix timestamp in milliseconds
31 updated_at INTEGER NOT NULL, -- Unix timestamp in milliseconds
32 FOREIGN KEY (session_id) REFERENCES sessions (id) ON DELETE CASCADE,
32 FOREIGN KEY (session_id) REFERENCES sessions (id) ON DELETE CASCADE,
internal/db/models.go+1−1
@@ -13,7 +13,7 @@ type File struct {
13 SessionID string `json:"session_id"`
13 SessionID string `json:"session_id"`
14 Path string `json:"path"`
14 Path string `json:"path"`
15 Content string `json:"content"`
15 Content string `json:"content"`
16− Version string `json:"version"`
16+ Version int64 `json:"version"`
17 CreatedAt int64 `json:"created_at"`
17 CreatedAt int64 `json:"created_at"`
18 UpdatedAt int64 `json:"updated_at"`
18 UpdatedAt int64 `json:"updated_at"`
19}
19}
internal/db/querier.go−1
@@ -27,7 +27,6 @@ type Querier interface {
27 ListMessagesBySession(ctx context.Context, sessionID string) ([]Message, error)
27 ListMessagesBySession(ctx context.Context, sessionID string) ([]Message, error)
28 ListNewFiles(ctx context.Context) ([]File, error)
28 ListNewFiles(ctx context.Context) ([]File, error)
29 ListSessions(ctx context.Context) ([]Session, error)
29 ListSessions(ctx context.Context) ([]Session, error)
30− UpdateFile(ctx context.Context, arg UpdateFileParams) (File, error)
31 UpdateMessage(ctx context.Context, arg UpdateMessageParams) error
30 UpdateMessage(ctx context.Context, arg UpdateMessageParams) error
32 UpdateSession(ctx context.Context, arg UpdateSessionParams) (Session, error)
31 UpdateSession(ctx context.Context, arg UpdateSessionParams) (Session, error)
33}
32}
internal/db/sql/files.sql+6−15
@@ -7,20 +7,20 @@ WHERE id = ? LIMIT 1;
7SELECT *
7SELECT *
8FROM files
8FROM files
9WHERE path = ? AND session_id = ?
9WHERE path = ? AND session_id = ?
10−ORDER BY created_at DESC
10+ORDER BY version DESC, created_at DESC
11LIMIT 1;
11LIMIT 1;
12
12
13-- name: ListFilesBySession :many
13-- name: ListFilesBySession :many
14SELECT *
14SELECT *
15FROM files
15FROM files
16WHERE session_id = ?
16WHERE session_id = ?
17−ORDER BY created_at ASC;
17+ORDER BY version ASC, created_at ASC;
18
18
19-- name: ListFilesByPath :many
19-- name: ListFilesByPath :many
20SELECT *
20SELECT *
21FROM files
21FROM files
22WHERE path = ?
22WHERE path = ?
23−ORDER BY created_at DESC;
23+ORDER BY version DESC, created_at DESC;
24
24
25-- name: CreateFile :one
25-- name: CreateFile :one
26INSERT INTO files (
26INSERT INTO files (
@@ -36,15 +36,6 @@ INSERT INTO files (
36)
36)
37RETURNING *;
37RETURNING *;
38
38
39−-- name: UpdateFile :one
40−UPDATE files
41−SET
42− content = ?,
43− version = ?,
44− updated_at = strftime('%s', 'now')
45−WHERE id = ?
46−RETURNING *;
47−
48-- name: DeleteFile :exec
39-- name: DeleteFile :exec
49DELETE FROM files
40DELETE FROM files
50WHERE id = ?;
41WHERE id = ?;
@@ -57,10 +48,10 @@ WHERE session_id = ?;
57SELECT f.*
48SELECT f.*
58FROM files f
49FROM files f
59INNER JOIN (
50INNER JOIN (
60− SELECT path, MAX(created_at) as max_created_at
51+ SELECT path, MAX(version) as max_version, MAX(created_at) as max_created_at
61 FROM files
52 FROM files
62 GROUP BY path
53 GROUP BY path
63−) latest ON f.path = latest.path AND f.created_at = latest.max_created_at
54+) latest ON f.path = latest.path AND f.version = latest.max_version AND f.created_at = latest.max_created_at
64WHERE f.session_id = ?
55WHERE f.session_id = ?
65ORDER BY f.path;
56ORDER BY f.path;
66
57
@@ -68,4 +59,4 @@ ORDER BY f.path;
68SELECT *
59SELECT *
69FROM files
60FROM files
70WHERE is_new = 1
61WHERE is_new = 1
71−ORDER BY created_at DESC;
62+ORDER BY version DESC, created_at DESC;
internal/history/file.go+7−49
@@ -4,9 +4,7 @@ import (
4 "context"
4 "context"
5 "database/sql"
5 "database/sql"
6 "fmt"
6 "fmt"
7− "strconv"
8 "strings"
7 "strings"
9− "time"
10
8
11 "github.com/charmbracelet/crush/internal/db"
9 "github.com/charmbracelet/crush/internal/db"
12 "github.com/charmbracelet/crush/internal/pubsub"
10 "github.com/charmbracelet/crush/internal/pubsub"
@@ -14,7 +12,7 @@ import (
14)
12)
15
13
16const (
14const (
17− InitialVersion = "initial"
15+ InitialVersion = 0
18)
16)
19
17
20type File struct {
18type File struct {
@@ -22,7 +20,7 @@ type File struct {
22 SessionID string
20 SessionID string
23 Path string
21 Path string
24 Content string
22 Content string
25− Version string
23+ Version int64
26 CreatedAt int64
24 CreatedAt int64
27 UpdatedAt int64
25 UpdatedAt int64
28}
26}
@@ -35,7 +33,6 @@ type Service interface {
35 GetByPathAndSession(ctx context.Context, path, sessionID string) (File, error)
33 GetByPathAndSession(ctx context.Context, path, sessionID string) (File, error)
36 ListBySession(ctx context.Context, sessionID string) ([]File, error)
34 ListBySession(ctx context.Context, sessionID string) ([]File, error)
37 ListLatestSessionFiles(ctx context.Context, sessionID string) ([]File, error)
35 ListLatestSessionFiles(ctx context.Context, sessionID string) ([]File, error)
38− Update(ctx context.Context, file File) (File, error)
39 Delete(ctx context.Context, id string) error
36 Delete(ctx context.Context, id string) error
40 DeleteSessionFiles(ctx context.Context, sessionID string) error
37 DeleteSessionFiles(ctx context.Context, sessionID string) error
41}
38}
@@ -71,30 +68,13 @@ func (s *service) CreateVersion(ctx context.Context, sessionID, path, content st
71 }
68 }
72
69
73 // Get the latest version
70 // Get the latest version
74− latestFile := files[0] // Files are ordered by created_at DESC
71+ latestFile := files[0] // Files are ordered by version DESC, created_at DESC
75− latestVersion := latestFile.Version
72+ nextVersion := latestFile.Version + 1
76−
77− // Generate the next version
78− var nextVersion string
79− if latestVersion == InitialVersion {
80− nextVersion = "v1"
81− } else if strings.HasPrefix(latestVersion, "v") {
82− versionNum, err := strconv.Atoi(latestVersion[1:])
83− if err != nil {
84− // If we can't parse the version, just use a timestamp-based version
85− nextVersion = fmt.Sprintf("v%d", latestFile.CreatedAt)
86− } else {
87− nextVersion = fmt.Sprintf("v%d", versionNum+1)
88− }
89− } else {
90− // If the version format is unexpected, use a timestamp-based version
91− nextVersion = fmt.Sprintf("v%d", latestFile.CreatedAt)
92− }
93
73
94 return s.createWithVersion(ctx, sessionID, path, content, nextVersion)
74 return s.createWithVersion(ctx, sessionID, path, content, nextVersion)
95}
75}
96
76
97−func (s *service) createWithVersion(ctx context.Context, sessionID, path, content, version string) (File, error) {
77+func (s *service) createWithVersion(ctx context.Context, sessionID, path, content string, version int64) (File, error) {
98 // Maximum number of retries for transaction conflicts
78 // Maximum number of retries for transaction conflicts
99 const maxRetries = 3
79 const maxRetries = 3
100 var file File
80 var file File
@@ -126,16 +106,8 @@ func (s *service) createWithVersion(ctx context.Context, sessionID, path, conten
126 // Check if this is a uniqueness constraint violation
106 // Check if this is a uniqueness constraint violation
127 if strings.Contains(txErr.Error(), "UNIQUE constraint failed") {
107 if strings.Contains(txErr.Error(), "UNIQUE constraint failed") {
128 if attempt < maxRetries-1 {
108 if attempt < maxRetries-1 {
129− // If we have retries left, generate a new version and try again
109+ // If we have retries left, increment version and try again
130− if strings.HasPrefix(version, "v") {
110+ version++
131− versionNum, parseErr := strconv.Atoi(version[1:])
132− if parseErr == nil {
133− version = fmt.Sprintf("v%d", versionNum+1)
134− continue
135− }
136− }
137− // If we can't parse the version, use a timestamp-based version
138− version = fmt.Sprintf("v%d", time.Now().Unix())
139 continue
111 continue
140 }
112 }
141 }
113 }
@@ -198,20 +170,6 @@ func (s *service) ListLatestSessionFiles(ctx context.Context, sessionID string)
198 return files, nil
170 return files, nil
199}
171}
200
172
201−func (s *service) Update(ctx context.Context, file File) (File, error) {
202− dbFile, err := s.q.UpdateFile(ctx, db.UpdateFileParams{
203− ID: file.ID,
204− Content: file.Content,
205− Version: file.Version,
206− })
207− if err != nil {
208− return File{}, err
209− }
210− updatedFile := s.fromDBItem(dbFile)
211− s.Publish(pubsub.UpdatedEvent, updatedFile)
212− return updatedFile, nil
213−}
214−
215func (s *service) Delete(ctx context.Context, id string) error {
173func (s *service) Delete(ctx context.Context, id string) error {
216 file, err := s.Get(ctx, id)
174 file, err := s.Get(ctx, id)
217 if err != nil {
175 if err != nil {
internal/llm/tools/shell/comparison_test.go−1
@@ -31,7 +31,6 @@ func TestShellPerformanceComparison(t *testing.T) {
31 t.Logf("Quick command took: %v", duration)
31 t.Logf("Quick command took: %v", duration)
32}
32}
33
33
34−
35// Benchmark CPU usage during polling
34// Benchmark CPU usage during polling
36func BenchmarkShellPolling(b *testing.B) {
35func BenchmarkShellPolling(b *testing.B) {
37 tmpDir, err := os.MkdirTemp("", "shell-bench")
36 tmpDir, err := os.MkdirTemp("", "shell-bench")
internal/tui/components/chat/sidebar/sidebar.go+83−26
@@ -4,7 +4,9 @@ import (
4 "context"
4 "context"
5 "fmt"
5 "fmt"
6 "os"
6 "os"
7+ "sort"
7 "strings"
8 "strings"
9+ "sync"
8
10
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
33)
35)
34
36
37+type FileHistory struct {
38+ initialVersion history.File
39+ latestVersion history.File
40+}
41+
35type SessionFile struct {
42type SessionFile struct {
43+ History FileHistory
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
65+ files sync.Map
57}
66}
58
67
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) {
77 }
86 }
78 return m, m.loadSessionFiles
87 return m, m.loadSessionFiles
79 case SessionFilesMsg:
88 case SessionFilesMsg:
89+ m.files = sync.Map{}
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)
92+ }
82 return m, nil
93 return m, nil
83
94
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 {
123 )
137 )
124}
138}
125
139
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+
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 {
132 }
190 }
133 }
191 }
134
192
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)
141
194
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
147− }
148− if file.Version == history.InitialVersion {
149− existing.initialVersion = file
150− }
151 fileMap[file.Path] = existing
199 fileMap[file.Path] = existing
152 } else {
200 } else {
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,
157 }
205 }
@@ -160,14 +208,13 @@ func (m *sidebarCmp) loadSessionFiles() tea.Msg {
160
208
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{
213+ History: fh,
166− FilePath: path,
214+ FilePath: path,
167− Additions: additions,
215+ Additions: additions,
168− Deletions: deletions,
216+ Deletions: deletions,
169− })
170− }
217+ })
171 }
218 }
172
219
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),
211 )
258 )
212
259
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+ })
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,
216 section,
269 section,
@@ -220,8 +273,12 @@ func (m *sidebarCmp) filesBlock() string {
220 }
273 }
221
274
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
279+ })
223
280
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
226
283
227 // Create status indicators for additions/deletions
284 // Create status indicators for additions/deletions
todos.md+3−1
@@ -5,7 +5,9 @@
5 - [x] Make help dependent on the focused pane and page
5 - [x] Make help dependent on the focused pane and page
6- [x] Implement current model in the sidebar
6- [x] Implement current model in the sidebar
7- [x] Implement LSP errors
7- [x] Implement LSP errors
8−- [ ] Implement changed files
8+- [x] Implement changed files
9+ - [x] Implement initial load
10+ - [x] Implement realtime file changes
9- [ ] Events when tool error
11- [ ] Events when tool error
10- [ ] Support bash commands
12- [ ] Support bash commands
11- [ ] Editor attachments fixes
13- [ ] Editor attachments fixes