Detailed changes
@@ -256,6 +256,7 @@ func setupSubscriptions(app *app.App, parentCtx context.Context) (chan tea.Msg,
setupSubscriber(ctx, &wg, "messages", app.Messages.Subscribe, ch)
setupSubscriber(ctx, &wg, "permissions", app.Permissions.Subscribe, ch)
setupSubscriber(ctx, &wg, "coderAgent", app.CoderAgent.Subscribe, ch)
+ setupSubscriber(ctx, &wg, "history", app.History.Subscribe, ch)
cleanupFunc := func() {
logging.Info("Cancelling all subscriptions")
@@ -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"}
+{"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"]}
@@ -78,9 +78,6 @@ func Prepare(ctx context.Context, db DBTX) (*Queries, error) {
if q.listSessionsStmt, err = db.PrepareContext(ctx, listSessions); err != nil {
return nil, fmt.Errorf("error preparing query ListSessions: %w", err)
}
- if q.updateFileStmt, err = db.PrepareContext(ctx, updateFile); err != nil {
- return nil, fmt.Errorf("error preparing query UpdateFile: %w", err)
- }
if q.updateMessageStmt, err = db.PrepareContext(ctx, updateMessage); err != nil {
return nil, fmt.Errorf("error preparing query UpdateMessage: %w", err)
}
@@ -182,11 +179,6 @@ func (q *Queries) Close() error {
err = fmt.Errorf("error closing listSessionsStmt: %w", cerr)
}
}
- if q.updateFileStmt != nil {
- if cerr := q.updateFileStmt.Close(); cerr != nil {
- err = fmt.Errorf("error closing updateFileStmt: %w", cerr)
- }
- }
if q.updateMessageStmt != nil {
if cerr := q.updateMessageStmt.Close(); cerr != nil {
err = fmt.Errorf("error closing updateMessageStmt: %w", cerr)
@@ -254,7 +246,6 @@ type Queries struct {
listMessagesBySessionStmt *sql.Stmt
listNewFilesStmt *sql.Stmt
listSessionsStmt *sql.Stmt
- updateFileStmt *sql.Stmt
updateMessageStmt *sql.Stmt
updateSessionStmt *sql.Stmt
}
@@ -281,7 +272,6 @@ func (q *Queries) WithTx(tx *sql.Tx) *Queries {
listMessagesBySessionStmt: q.listMessagesBySessionStmt,
listNewFilesStmt: q.listNewFilesStmt,
listSessionsStmt: q.listSessionsStmt,
- updateFileStmt: q.updateFileStmt,
updateMessageStmt: q.updateMessageStmt,
updateSessionStmt: q.updateSessionStmt,
}
@@ -29,7 +29,7 @@ type CreateFileParams struct {
SessionID string `json:"session_id"`
Path string `json:"path"`
Content string `json:"content"`
- Version string `json:"version"`
+ Version int64 `json:"version"`
}
func (q *Queries) CreateFile(ctx context.Context, arg CreateFileParams) (File, error) {
@@ -98,7 +98,7 @@ const getFileByPathAndSession = `-- name: GetFileByPathAndSession :one
SELECT id, session_id, path, content, version, created_at, updated_at
FROM files
WHERE path = ? AND session_id = ?
-ORDER BY created_at DESC
+ORDER BY version DESC, created_at DESC
LIMIT 1
`
@@ -126,7 +126,7 @@ const listFilesByPath = `-- name: ListFilesByPath :many
SELECT id, session_id, path, content, version, created_at, updated_at
FROM files
WHERE path = ?
-ORDER BY created_at DESC
+ORDER BY version DESC, created_at DESC
`
func (q *Queries) ListFilesByPath(ctx context.Context, path string) ([]File, error) {
@@ -164,7 +164,7 @@ const listFilesBySession = `-- name: ListFilesBySession :many
SELECT id, session_id, path, content, version, created_at, updated_at
FROM files
WHERE session_id = ?
-ORDER BY created_at ASC
+ORDER BY version ASC, created_at ASC
`
func (q *Queries) ListFilesBySession(ctx context.Context, sessionID string) ([]File, error) {
@@ -202,10 +202,10 @@ const listLatestSessionFiles = `-- name: ListLatestSessionFiles :many
SELECT f.id, f.session_id, f.path, f.content, f.version, f.created_at, f.updated_at
FROM files f
INNER JOIN (
- SELECT path, MAX(created_at) as max_created_at
+ SELECT path, MAX(version) as max_version, MAX(created_at) as max_created_at
FROM files
GROUP BY path
-) latest ON f.path = latest.path AND f.created_at = latest.max_created_at
+) latest ON f.path = latest.path AND f.version = latest.max_version AND f.created_at = latest.max_created_at
WHERE f.session_id = ?
ORDER BY f.path
`
@@ -245,7 +245,7 @@ const listNewFiles = `-- name: ListNewFiles :many
SELECT id, session_id, path, content, version, created_at, updated_at
FROM files
WHERE is_new = 1
-ORDER BY created_at DESC
+ORDER BY version DESC, created_at DESC
`
func (q *Queries) ListNewFiles(ctx context.Context) ([]File, error) {
@@ -278,34 +278,3 @@ func (q *Queries) ListNewFiles(ctx context.Context) ([]File, error) {
}
return items, nil
}
-
-const updateFile = `-- name: UpdateFile :one
-UPDATE files
-SET
- content = ?,
- version = ?,
- updated_at = strftime('%s', 'now')
-WHERE id = ?
-RETURNING id, session_id, path, content, version, created_at, updated_at
-`
-
-type UpdateFileParams struct {
- Content string `json:"content"`
- Version string `json:"version"`
- ID string `json:"id"`
-}
-
-func (q *Queries) UpdateFile(ctx context.Context, arg UpdateFileParams) (File, error) {
- row := q.queryRow(ctx, q.updateFileStmt, updateFile, arg.Content, arg.Version, arg.ID)
- var i File
- err := row.Scan(
- &i.ID,
- &i.SessionID,
- &i.Path,
- &i.Content,
- &i.Version,
- &i.CreatedAt,
- &i.UpdatedAt,
- )
- return i, err
-}
@@ -26,7 +26,7 @@ CREATE TABLE IF NOT EXISTS files (
session_id TEXT NOT NULL,
path TEXT NOT NULL,
content TEXT NOT NULL,
- version TEXT NOT NULL,
+ version INTEGER NOT NULL DEFAULT 0,
created_at INTEGER NOT NULL, -- Unix timestamp in milliseconds
updated_at INTEGER NOT NULL, -- Unix timestamp in milliseconds
FOREIGN KEY (session_id) REFERENCES sessions (id) ON DELETE CASCADE,
@@ -13,7 +13,7 @@ type File struct {
SessionID string `json:"session_id"`
Path string `json:"path"`
Content string `json:"content"`
- Version string `json:"version"`
+ Version int64 `json:"version"`
CreatedAt int64 `json:"created_at"`
UpdatedAt int64 `json:"updated_at"`
}
@@ -27,7 +27,6 @@ type Querier interface {
ListMessagesBySession(ctx context.Context, sessionID string) ([]Message, error)
ListNewFiles(ctx context.Context) ([]File, error)
ListSessions(ctx context.Context) ([]Session, error)
- UpdateFile(ctx context.Context, arg UpdateFileParams) (File, error)
UpdateMessage(ctx context.Context, arg UpdateMessageParams) error
UpdateSession(ctx context.Context, arg UpdateSessionParams) (Session, error)
}
@@ -7,20 +7,20 @@ WHERE id = ? LIMIT 1;
SELECT *
FROM files
WHERE path = ? AND session_id = ?
-ORDER BY created_at DESC
+ORDER BY version DESC, created_at DESC
LIMIT 1;
-- name: ListFilesBySession :many
SELECT *
FROM files
WHERE session_id = ?
-ORDER BY created_at ASC;
+ORDER BY version ASC, created_at ASC;
-- name: ListFilesByPath :many
SELECT *
FROM files
WHERE path = ?
-ORDER BY created_at DESC;
+ORDER BY version DESC, created_at DESC;
-- name: CreateFile :one
INSERT INTO files (
@@ -36,15 +36,6 @@ INSERT INTO files (
)
RETURNING *;
--- name: UpdateFile :one
-UPDATE files
-SET
- content = ?,
- version = ?,
- updated_at = strftime('%s', 'now')
-WHERE id = ?
-RETURNING *;
-
-- name: DeleteFile :exec
DELETE FROM files
WHERE id = ?;
@@ -57,10 +48,10 @@ WHERE session_id = ?;
SELECT f.*
FROM files f
INNER JOIN (
- SELECT path, MAX(created_at) as max_created_at
+ SELECT path, MAX(version) as max_version, MAX(created_at) as max_created_at
FROM files
GROUP BY path
-) latest ON f.path = latest.path AND f.created_at = latest.max_created_at
+) latest ON f.path = latest.path AND f.version = latest.max_version AND f.created_at = latest.max_created_at
WHERE f.session_id = ?
ORDER BY f.path;
@@ -68,4 +59,4 @@ ORDER BY f.path;
SELECT *
FROM files
WHERE is_new = 1
-ORDER BY created_at DESC;
+ORDER BY version DESC, created_at DESC;
@@ -4,9 +4,7 @@ import (
"context"
"database/sql"
"fmt"
- "strconv"
"strings"
- "time"
"github.com/charmbracelet/crush/internal/db"
"github.com/charmbracelet/crush/internal/pubsub"
@@ -14,7 +12,7 @@ import (
)
const (
- InitialVersion = "initial"
+ InitialVersion = 0
)
type File struct {
@@ -22,7 +20,7 @@ type File struct {
SessionID string
Path string
Content string
- Version string
+ Version int64
CreatedAt int64
UpdatedAt int64
}
@@ -35,7 +33,6 @@ type Service interface {
GetByPathAndSession(ctx context.Context, path, sessionID string) (File, error)
ListBySession(ctx context.Context, sessionID string) ([]File, error)
ListLatestSessionFiles(ctx context.Context, sessionID string) ([]File, error)
- Update(ctx context.Context, file File) (File, error)
Delete(ctx context.Context, id string) error
DeleteSessionFiles(ctx context.Context, sessionID string) error
}
@@ -71,30 +68,13 @@ func (s *service) CreateVersion(ctx context.Context, sessionID, path, content st
}
// Get the latest version
- latestFile := files[0] // Files are ordered by created_at DESC
- latestVersion := latestFile.Version
-
- // Generate the next version
- var nextVersion string
- if latestVersion == InitialVersion {
- nextVersion = "v1"
- } else if strings.HasPrefix(latestVersion, "v") {
- versionNum, err := strconv.Atoi(latestVersion[1:])
- if err != nil {
- // If we can't parse the version, just use a timestamp-based version
- nextVersion = fmt.Sprintf("v%d", latestFile.CreatedAt)
- } else {
- nextVersion = fmt.Sprintf("v%d", versionNum+1)
- }
- } else {
- // If the version format is unexpected, use a timestamp-based version
- nextVersion = fmt.Sprintf("v%d", latestFile.CreatedAt)
- }
+ latestFile := files[0] // Files are ordered by version DESC, created_at DESC
+ nextVersion := latestFile.Version + 1
return s.createWithVersion(ctx, sessionID, path, content, nextVersion)
}
-func (s *service) createWithVersion(ctx context.Context, sessionID, path, content, version string) (File, error) {
+func (s *service) createWithVersion(ctx context.Context, sessionID, path, content string, version int64) (File, error) {
// Maximum number of retries for transaction conflicts
const maxRetries = 3
var file File
@@ -126,16 +106,8 @@ func (s *service) createWithVersion(ctx context.Context, sessionID, path, conten
// Check if this is a uniqueness constraint violation
if strings.Contains(txErr.Error(), "UNIQUE constraint failed") {
if attempt < maxRetries-1 {
- // If we have retries left, generate a new version and try again
- if strings.HasPrefix(version, "v") {
- versionNum, parseErr := strconv.Atoi(version[1:])
- if parseErr == nil {
- version = fmt.Sprintf("v%d", versionNum+1)
- continue
- }
- }
- // If we can't parse the version, use a timestamp-based version
- version = fmt.Sprintf("v%d", time.Now().Unix())
+ // If we have retries left, increment version and try again
+ version++
continue
}
}
@@ -198,20 +170,6 @@ func (s *service) ListLatestSessionFiles(ctx context.Context, sessionID string)
return files, nil
}
-func (s *service) Update(ctx context.Context, file File) (File, error) {
- dbFile, err := s.q.UpdateFile(ctx, db.UpdateFileParams{
- ID: file.ID,
- Content: file.Content,
- Version: file.Version,
- })
- if err != nil {
- return File{}, err
- }
- updatedFile := s.fromDBItem(dbFile)
- s.Publish(pubsub.UpdatedEvent, updatedFile)
- return updatedFile, nil
-}
-
func (s *service) Delete(ctx context.Context, id string) error {
file, err := s.Get(ctx, id)
if err != nil {
@@ -31,7 +31,6 @@ func TestShellPerformanceComparison(t *testing.T) {
t.Logf("Quick command took: %v", duration)
}
-
// Benchmark CPU usage during polling
func BenchmarkShellPolling(b *testing.B) {
tmpDir, err := os.MkdirTemp("", "shell-bench")
@@ -4,7 +4,9 @@ import (
"context"
"fmt"
"os"
+ "sort"
"strings"
+ "sync"
tea "github.com/charmbracelet/bubbletea/v2"
"github.com/charmbracelet/crush/internal/config"
@@ -32,7 +34,13 @@ const (
logoBreakpoint = 65
)
+type FileHistory struct {
+ initialVersion history.File
+ latestVersion history.File
+}
+
type SessionFile struct {
+ History FileHistory
FilePath string
Additions int
Deletions int
@@ -53,7 +61,8 @@ type sidebarCmp struct {
cwd string
lspClients map[string]*lsp.Client
history history.Service
- files []SessionFile
+ // Using a sync map here because we might receive file history events concurrently
+ files sync.Map
}
func 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) {
}
return m, m.loadSessionFiles
case SessionFilesMsg:
- m.files = msg.Files
- logging.Info("Loaded session files", "count", len(m.files))
+ m.files = sync.Map{}
+ for _, file := range msg.Files {
+ m.files.Store(file.FilePath, file)
+ }
return m, nil
case chat.SessionClearedMsg:
m.session = session.Session{}
+ case pubsub.Event[history.File]:
+ logging.Info("sidebar", "Received file history event", "file", msg.Payload.Path, "session", msg.Payload.SessionID)
+ return m, m.handleFileHistoryEvent(msg)
case pubsub.Event[session.Session]:
if msg.Type == pubsub.UpdatedEvent {
if m.session.ID == msg.Payload.ID {
@@ -123,6 +137,50 @@ func (m *sidebarCmp) View() tea.View {
)
}
+func (m *sidebarCmp) handleFileHistoryEvent(event pubsub.Event[history.File]) tea.Cmd {
+ return func() tea.Msg {
+ file := event.Payload
+ found := false
+ m.files.Range(func(key, value any) bool {
+ existing := value.(SessionFile)
+ if existing.FilePath == file.Path {
+ if existing.History.latestVersion.Version < file.Version {
+ existing.History.latestVersion = file
+ } else if file.Version == 0 {
+ existing.History.initialVersion = file
+ } else {
+ // If the version is not greater than the latest, we ignore it
+ return true
+ }
+ before := existing.History.initialVersion.Content
+ after := existing.History.latestVersion.Content
+ path := existing.History.initialVersion.Path
+ _, additions, deletions := diff.GenerateDiff(before, after, path)
+ existing.Additions = additions
+ existing.Deletions = deletions
+ m.files.Store(file.Path, existing)
+ found = true
+ return false
+ }
+ return true
+ })
+ if found {
+ return nil
+ }
+ sf := SessionFile{
+ History: FileHistory{
+ initialVersion: file,
+ latestVersion: file,
+ },
+ FilePath: file.Path,
+ Additions: 0,
+ Deletions: 0,
+ }
+ m.files.Store(file.Path, sf)
+ return nil
+ }
+}
+
func (m *sidebarCmp) loadSessionFiles() tea.Msg {
files, err := m.history.ListBySession(context.Background(), m.session.ID)
if err != nil {
@@ -132,26 +190,16 @@ func (m *sidebarCmp) loadSessionFiles() tea.Msg {
}
}
- type fileHistory struct {
- initialVersion history.File
- latestVersion history.File
- }
-
- fileMap := make(map[string]fileHistory)
+ fileMap := make(map[string]FileHistory)
for _, file := range files {
if existing, ok := fileMap[file.Path]; ok {
// Update the latest version
- if existing.latestVersion.CreatedAt < file.CreatedAt {
- existing.latestVersion = file
- }
- if file.Version == history.InitialVersion {
- existing.initialVersion = file
- }
+ existing.latestVersion = file
fileMap[file.Path] = existing
} else {
// Add the initial version
- fileMap[file.Path] = fileHistory{
+ fileMap[file.Path] = FileHistory{
initialVersion: file,
latestVersion: file,
}
@@ -160,14 +208,13 @@ func (m *sidebarCmp) loadSessionFiles() tea.Msg {
sessionFiles := make([]SessionFile, 0, len(fileMap))
for path, fh := range fileMap {
- if fh.initialVersion.Version == history.InitialVersion {
- _, additions, deletions := diff.GenerateDiff(fh.initialVersion.Content, fh.latestVersion.Content, fh.initialVersion.Path)
- sessionFiles = append(sessionFiles, SessionFile{
- FilePath: path,
- Additions: additions,
- Deletions: deletions,
- })
- }
+ _, additions, deletions := diff.GenerateDiff(fh.initialVersion.Content, fh.latestVersion.Content, fh.initialVersion.Path)
+ sessionFiles = append(sessionFiles, SessionFile{
+ History: fh,
+ FilePath: path,
+ Additions: additions,
+ Deletions: deletions,
+ })
}
return SessionFilesMsg{
@@ -210,7 +257,13 @@ func (m *sidebarCmp) filesBlock() string {
core.Section("Modified Files", maxWidth),
)
- if len(m.files) == 0 {
+ files := make([]SessionFile, 0)
+ m.files.Range(func(key, value any) bool {
+ file := value.(SessionFile)
+ files = append(files, file)
+ return true // continue iterating
+ })
+ if len(files) == 0 {
return lipgloss.JoinVertical(
lipgloss.Left,
section,
@@ -220,8 +273,12 @@ func (m *sidebarCmp) filesBlock() string {
}
fileList := []string{section, ""}
+ // order files by the latest version's created time
+ sort.Slice(files, func(i, j int) bool {
+ return files[i].History.latestVersion.CreatedAt > files[j].History.latestVersion.CreatedAt
+ })
- for _, file := range m.files {
+ for _, file := range files {
// Extract just the filename from the path
// Create status indicators for additions/deletions
@@ -5,7 +5,9 @@
- [x] Make help dependent on the focused pane and page
- [x] Implement current model in the sidebar
- [x] Implement LSP errors
-- [ ] Implement changed files
+- [x] Implement changed files
+ - [x] Implement initial load
+ - [x] Implement realtime file changes
- [ ] Events when tool error
- [ ] Support bash commands
- [ ] Editor attachments fixes