mirror of
https://github.com/charmbracelet/crush.git
synced 2025-08-02 05:20:46 +03:00
fix: fix the file history table and implement realtime file updates
This commit is contained in:
@@ -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
|
||||
|
||||
4
todos.md
4
todos.md
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user