Detailed changes
@@ -6,6 +6,7 @@ import (
"github.com/kujtimiihoxha/termai/internal/config"
"github.com/kujtimiihoxha/termai/internal/db"
+ "github.com/kujtimiihoxha/termai/internal/history"
"github.com/kujtimiihoxha/termai/internal/logging"
"github.com/kujtimiihoxha/termai/internal/lsp"
"github.com/kujtimiihoxha/termai/internal/lsp/watcher"
@@ -19,6 +20,7 @@ type App struct {
Sessions session.Service
Messages message.Service
+ Files history.Service
Permissions permission.Service
LSPClients map[string]*lsp.Client
@@ -31,11 +33,13 @@ func New(ctx context.Context, conn *sql.DB) *App {
q := db.New(conn)
sessions := session.NewService(ctx, q)
messages := message.NewService(ctx, q)
+ files := history.NewService(ctx, q)
app := &App{
Context: ctx,
Sessions: sessions,
Messages: messages,
+ Files: files,
Permissions: permission.NewPermissionService(),
LSPClients: make(map[string]*lsp.Client),
}
@@ -24,33 +24,63 @@ func New(db DBTX) *Queries {
func Prepare(ctx context.Context, db DBTX) (*Queries, error) {
q := Queries{db: db}
var err error
+ if q.createFileStmt, err = db.PrepareContext(ctx, createFile); err != nil {
+ return nil, fmt.Errorf("error preparing query CreateFile: %w", err)
+ }
if q.createMessageStmt, err = db.PrepareContext(ctx, createMessage); err != nil {
return nil, fmt.Errorf("error preparing query CreateMessage: %w", err)
}
if q.createSessionStmt, err = db.PrepareContext(ctx, createSession); err != nil {
return nil, fmt.Errorf("error preparing query CreateSession: %w", err)
}
+ if q.deleteFileStmt, err = db.PrepareContext(ctx, deleteFile); err != nil {
+ return nil, fmt.Errorf("error preparing query DeleteFile: %w", err)
+ }
if q.deleteMessageStmt, err = db.PrepareContext(ctx, deleteMessage); err != nil {
return nil, fmt.Errorf("error preparing query DeleteMessage: %w", err)
}
if q.deleteSessionStmt, err = db.PrepareContext(ctx, deleteSession); err != nil {
return nil, fmt.Errorf("error preparing query DeleteSession: %w", err)
}
+ if q.deleteSessionFilesStmt, err = db.PrepareContext(ctx, deleteSessionFiles); err != nil {
+ return nil, fmt.Errorf("error preparing query DeleteSessionFiles: %w", err)
+ }
if q.deleteSessionMessagesStmt, err = db.PrepareContext(ctx, deleteSessionMessages); err != nil {
return nil, fmt.Errorf("error preparing query DeleteSessionMessages: %w", err)
}
+ if q.getFileStmt, err = db.PrepareContext(ctx, getFile); err != nil {
+ return nil, fmt.Errorf("error preparing query GetFile: %w", err)
+ }
+ if q.getFileByPathAndSessionStmt, err = db.PrepareContext(ctx, getFileByPathAndSession); err != nil {
+ return nil, fmt.Errorf("error preparing query GetFileByPathAndSession: %w", err)
+ }
if q.getMessageStmt, err = db.PrepareContext(ctx, getMessage); err != nil {
return nil, fmt.Errorf("error preparing query GetMessage: %w", err)
}
if q.getSessionByIDStmt, err = db.PrepareContext(ctx, getSessionByID); err != nil {
return nil, fmt.Errorf("error preparing query GetSessionByID: %w", err)
}
+ if q.listFilesByPathStmt, err = db.PrepareContext(ctx, listFilesByPath); err != nil {
+ return nil, fmt.Errorf("error preparing query ListFilesByPath: %w", err)
+ }
+ if q.listFilesBySessionStmt, err = db.PrepareContext(ctx, listFilesBySession); err != nil {
+ return nil, fmt.Errorf("error preparing query ListFilesBySession: %w", err)
+ }
+ if q.listLatestSessionFilesStmt, err = db.PrepareContext(ctx, listLatestSessionFiles); err != nil {
+ return nil, fmt.Errorf("error preparing query ListLatestSessionFiles: %w", err)
+ }
if q.listMessagesBySessionStmt, err = db.PrepareContext(ctx, listMessagesBySession); err != nil {
return nil, fmt.Errorf("error preparing query ListMessagesBySession: %w", err)
}
+ if q.listNewFilesStmt, err = db.PrepareContext(ctx, listNewFiles); err != nil {
+ return nil, fmt.Errorf("error preparing query ListNewFiles: %w", err)
+ }
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)
}
@@ -62,6 +92,11 @@ func Prepare(ctx context.Context, db DBTX) (*Queries, error) {
func (q *Queries) Close() error {
var err error
+ if q.createFileStmt != nil {
+ if cerr := q.createFileStmt.Close(); cerr != nil {
+ err = fmt.Errorf("error closing createFileStmt: %w", cerr)
+ }
+ }
if q.createMessageStmt != nil {
if cerr := q.createMessageStmt.Close(); cerr != nil {
err = fmt.Errorf("error closing createMessageStmt: %w", cerr)
@@ -72,6 +107,11 @@ func (q *Queries) Close() error {
err = fmt.Errorf("error closing createSessionStmt: %w", cerr)
}
}
+ if q.deleteFileStmt != nil {
+ if cerr := q.deleteFileStmt.Close(); cerr != nil {
+ err = fmt.Errorf("error closing deleteFileStmt: %w", cerr)
+ }
+ }
if q.deleteMessageStmt != nil {
if cerr := q.deleteMessageStmt.Close(); cerr != nil {
err = fmt.Errorf("error closing deleteMessageStmt: %w", cerr)
@@ -82,11 +122,26 @@ func (q *Queries) Close() error {
err = fmt.Errorf("error closing deleteSessionStmt: %w", cerr)
}
}
+ if q.deleteSessionFilesStmt != nil {
+ if cerr := q.deleteSessionFilesStmt.Close(); cerr != nil {
+ err = fmt.Errorf("error closing deleteSessionFilesStmt: %w", cerr)
+ }
+ }
if q.deleteSessionMessagesStmt != nil {
if cerr := q.deleteSessionMessagesStmt.Close(); cerr != nil {
err = fmt.Errorf("error closing deleteSessionMessagesStmt: %w", cerr)
}
}
+ if q.getFileStmt != nil {
+ if cerr := q.getFileStmt.Close(); cerr != nil {
+ err = fmt.Errorf("error closing getFileStmt: %w", cerr)
+ }
+ }
+ if q.getFileByPathAndSessionStmt != nil {
+ if cerr := q.getFileByPathAndSessionStmt.Close(); cerr != nil {
+ err = fmt.Errorf("error closing getFileByPathAndSessionStmt: %w", cerr)
+ }
+ }
if q.getMessageStmt != nil {
if cerr := q.getMessageStmt.Close(); cerr != nil {
err = fmt.Errorf("error closing getMessageStmt: %w", cerr)
@@ -97,16 +152,41 @@ func (q *Queries) Close() error {
err = fmt.Errorf("error closing getSessionByIDStmt: %w", cerr)
}
}
+ if q.listFilesByPathStmt != nil {
+ if cerr := q.listFilesByPathStmt.Close(); cerr != nil {
+ err = fmt.Errorf("error closing listFilesByPathStmt: %w", cerr)
+ }
+ }
+ if q.listFilesBySessionStmt != nil {
+ if cerr := q.listFilesBySessionStmt.Close(); cerr != nil {
+ err = fmt.Errorf("error closing listFilesBySessionStmt: %w", cerr)
+ }
+ }
+ if q.listLatestSessionFilesStmt != nil {
+ if cerr := q.listLatestSessionFilesStmt.Close(); cerr != nil {
+ err = fmt.Errorf("error closing listLatestSessionFilesStmt: %w", cerr)
+ }
+ }
if q.listMessagesBySessionStmt != nil {
if cerr := q.listMessagesBySessionStmt.Close(); cerr != nil {
err = fmt.Errorf("error closing listMessagesBySessionStmt: %w", cerr)
}
}
+ if q.listNewFilesStmt != nil {
+ if cerr := q.listNewFilesStmt.Close(); cerr != nil {
+ err = fmt.Errorf("error closing listNewFilesStmt: %w", cerr)
+ }
+ }
if q.listSessionsStmt != nil {
if cerr := q.listSessionsStmt.Close(); cerr != nil {
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)
@@ -154,35 +234,55 @@ func (q *Queries) queryRow(ctx context.Context, stmt *sql.Stmt, query string, ar
}
type Queries struct {
- db DBTX
- tx *sql.Tx
- createMessageStmt *sql.Stmt
- createSessionStmt *sql.Stmt
- deleteMessageStmt *sql.Stmt
- deleteSessionStmt *sql.Stmt
- deleteSessionMessagesStmt *sql.Stmt
- getMessageStmt *sql.Stmt
- getSessionByIDStmt *sql.Stmt
- listMessagesBySessionStmt *sql.Stmt
- listSessionsStmt *sql.Stmt
- updateMessageStmt *sql.Stmt
- updateSessionStmt *sql.Stmt
+ db DBTX
+ tx *sql.Tx
+ createFileStmt *sql.Stmt
+ createMessageStmt *sql.Stmt
+ createSessionStmt *sql.Stmt
+ deleteFileStmt *sql.Stmt
+ deleteMessageStmt *sql.Stmt
+ deleteSessionStmt *sql.Stmt
+ deleteSessionFilesStmt *sql.Stmt
+ deleteSessionMessagesStmt *sql.Stmt
+ getFileStmt *sql.Stmt
+ getFileByPathAndSessionStmt *sql.Stmt
+ getMessageStmt *sql.Stmt
+ getSessionByIDStmt *sql.Stmt
+ listFilesByPathStmt *sql.Stmt
+ listFilesBySessionStmt *sql.Stmt
+ listLatestSessionFilesStmt *sql.Stmt
+ listMessagesBySessionStmt *sql.Stmt
+ listNewFilesStmt *sql.Stmt
+ listSessionsStmt *sql.Stmt
+ updateFileStmt *sql.Stmt
+ updateMessageStmt *sql.Stmt
+ updateSessionStmt *sql.Stmt
}
func (q *Queries) WithTx(tx *sql.Tx) *Queries {
return &Queries{
- db: tx,
- tx: tx,
- createMessageStmt: q.createMessageStmt,
- createSessionStmt: q.createSessionStmt,
- deleteMessageStmt: q.deleteMessageStmt,
- deleteSessionStmt: q.deleteSessionStmt,
- deleteSessionMessagesStmt: q.deleteSessionMessagesStmt,
- getMessageStmt: q.getMessageStmt,
- getSessionByIDStmt: q.getSessionByIDStmt,
- listMessagesBySessionStmt: q.listMessagesBySessionStmt,
- listSessionsStmt: q.listSessionsStmt,
- updateMessageStmt: q.updateMessageStmt,
- updateSessionStmt: q.updateSessionStmt,
+ db: tx,
+ tx: tx,
+ createFileStmt: q.createFileStmt,
+ createMessageStmt: q.createMessageStmt,
+ createSessionStmt: q.createSessionStmt,
+ deleteFileStmt: q.deleteFileStmt,
+ deleteMessageStmt: q.deleteMessageStmt,
+ deleteSessionStmt: q.deleteSessionStmt,
+ deleteSessionFilesStmt: q.deleteSessionFilesStmt,
+ deleteSessionMessagesStmt: q.deleteSessionMessagesStmt,
+ getFileStmt: q.getFileStmt,
+ getFileByPathAndSessionStmt: q.getFileByPathAndSessionStmt,
+ getMessageStmt: q.getMessageStmt,
+ getSessionByIDStmt: q.getSessionByIDStmt,
+ listFilesByPathStmt: q.listFilesByPathStmt,
+ listFilesBySessionStmt: q.listFilesBySessionStmt,
+ listLatestSessionFilesStmt: q.listLatestSessionFilesStmt,
+ listMessagesBySessionStmt: q.listMessagesBySessionStmt,
+ listNewFilesStmt: q.listNewFilesStmt,
+ listSessionsStmt: q.listSessionsStmt,
+ updateFileStmt: q.updateFileStmt,
+ updateMessageStmt: q.updateMessageStmt,
+ updateSessionStmt: q.updateSessionStmt,
}
}
@@ -0,0 +1,309 @@
+// Code generated by sqlc. DO NOT EDIT.
+// versions:
+// sqlc v1.27.0
+// source: files.sql
+
+package db
+
+import (
+ "context"
+)
+
+const createFile = `-- name: CreateFile :one
+INSERT INTO files (
+ id,
+ session_id,
+ path,
+ content,
+ version,
+ created_at,
+ updated_at
+) VALUES (
+ ?, ?, ?, ?, ?, strftime('%s', 'now'), strftime('%s', 'now')
+)
+RETURNING id, session_id, path, content, version, created_at, updated_at
+`
+
+type CreateFileParams struct {
+ ID string `json:"id"`
+ SessionID string `json:"session_id"`
+ Path string `json:"path"`
+ Content string `json:"content"`
+ Version string `json:"version"`
+}
+
+func (q *Queries) CreateFile(ctx context.Context, arg CreateFileParams) (File, error) {
+ row := q.queryRow(ctx, q.createFileStmt, createFile,
+ arg.ID,
+ arg.SessionID,
+ arg.Path,
+ arg.Content,
+ arg.Version,
+ )
+ var i File
+ err := row.Scan(
+ &i.ID,
+ &i.SessionID,
+ &i.Path,
+ &i.Content,
+ &i.Version,
+ &i.CreatedAt,
+ &i.UpdatedAt,
+ )
+ return i, err
+}
+
+const deleteFile = `-- name: DeleteFile :exec
+DELETE FROM files
+WHERE id = ?
+`
+
+func (q *Queries) DeleteFile(ctx context.Context, id string) error {
+ _, err := q.exec(ctx, q.deleteFileStmt, deleteFile, id)
+ return err
+}
+
+const deleteSessionFiles = `-- name: DeleteSessionFiles :exec
+DELETE FROM files
+WHERE session_id = ?
+`
+
+func (q *Queries) DeleteSessionFiles(ctx context.Context, sessionID string) error {
+ _, err := q.exec(ctx, q.deleteSessionFilesStmt, deleteSessionFiles, sessionID)
+ return err
+}
+
+const getFile = `-- name: GetFile :one
+SELECT id, session_id, path, content, version, created_at, updated_at
+FROM files
+WHERE id = ? LIMIT 1
+`
+
+func (q *Queries) GetFile(ctx context.Context, id string) (File, error) {
+ row := q.queryRow(ctx, q.getFileStmt, getFile, id)
+ var i File
+ err := row.Scan(
+ &i.ID,
+ &i.SessionID,
+ &i.Path,
+ &i.Content,
+ &i.Version,
+ &i.CreatedAt,
+ &i.UpdatedAt,
+ )
+ return i, err
+}
+
+const getFileByPathAndSession = `-- name: GetFileByPathAndSession :one
+SELECT id, session_id, path, content, version, created_at, updated_at
+FROM files
+WHERE path = ? AND session_id = ? LIMIT 1
+`
+
+type GetFileByPathAndSessionParams struct {
+ Path string `json:"path"`
+ SessionID string `json:"session_id"`
+}
+
+func (q *Queries) GetFileByPathAndSession(ctx context.Context, arg GetFileByPathAndSessionParams) (File, error) {
+ row := q.queryRow(ctx, q.getFileByPathAndSessionStmt, getFileByPathAndSession, arg.Path, arg.SessionID)
+ var i File
+ err := row.Scan(
+ &i.ID,
+ &i.SessionID,
+ &i.Path,
+ &i.Content,
+ &i.Version,
+ &i.CreatedAt,
+ &i.UpdatedAt,
+ )
+ return i, err
+}
+
+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
+`
+
+func (q *Queries) ListFilesByPath(ctx context.Context, path string) ([]File, error) {
+ rows, err := q.query(ctx, q.listFilesByPathStmt, listFilesByPath, path)
+ if err != nil {
+ return nil, err
+ }
+ defer rows.Close()
+ items := []File{}
+ for rows.Next() {
+ var i File
+ if err := rows.Scan(
+ &i.ID,
+ &i.SessionID,
+ &i.Path,
+ &i.Content,
+ &i.Version,
+ &i.CreatedAt,
+ &i.UpdatedAt,
+ ); err != nil {
+ return nil, err
+ }
+ items = append(items, i)
+ }
+ if err := rows.Close(); err != nil {
+ return nil, err
+ }
+ if err := rows.Err(); err != nil {
+ return nil, err
+ }
+ return items, nil
+}
+
+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
+`
+
+func (q *Queries) ListFilesBySession(ctx context.Context, sessionID string) ([]File, error) {
+ rows, err := q.query(ctx, q.listFilesBySessionStmt, listFilesBySession, sessionID)
+ if err != nil {
+ return nil, err
+ }
+ defer rows.Close()
+ items := []File{}
+ for rows.Next() {
+ var i File
+ if err := rows.Scan(
+ &i.ID,
+ &i.SessionID,
+ &i.Path,
+ &i.Content,
+ &i.Version,
+ &i.CreatedAt,
+ &i.UpdatedAt,
+ ); err != nil {
+ return nil, err
+ }
+ items = append(items, i)
+ }
+ if err := rows.Close(); err != nil {
+ return nil, err
+ }
+ if err := rows.Err(); err != nil {
+ return nil, err
+ }
+ return items, nil
+}
+
+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
+ FROM files
+ GROUP BY path
+) latest ON f.path = latest.path AND f.created_at = latest.max_created_at
+WHERE f.session_id = ?
+ORDER BY f.path
+`
+
+func (q *Queries) ListLatestSessionFiles(ctx context.Context, sessionID string) ([]File, error) {
+ rows, err := q.query(ctx, q.listLatestSessionFilesStmt, listLatestSessionFiles, sessionID)
+ if err != nil {
+ return nil, err
+ }
+ defer rows.Close()
+ items := []File{}
+ for rows.Next() {
+ var i File
+ if err := rows.Scan(
+ &i.ID,
+ &i.SessionID,
+ &i.Path,
+ &i.Content,
+ &i.Version,
+ &i.CreatedAt,
+ &i.UpdatedAt,
+ ); err != nil {
+ return nil, err
+ }
+ items = append(items, i)
+ }
+ if err := rows.Close(); err != nil {
+ return nil, err
+ }
+ if err := rows.Err(); err != nil {
+ return nil, err
+ }
+ return items, nil
+}
+
+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
+`
+
+func (q *Queries) ListNewFiles(ctx context.Context) ([]File, error) {
+ rows, err := q.query(ctx, q.listNewFilesStmt, listNewFiles)
+ if err != nil {
+ return nil, err
+ }
+ defer rows.Close()
+ items := []File{}
+ for rows.Next() {
+ var i File
+ if err := rows.Scan(
+ &i.ID,
+ &i.SessionID,
+ &i.Path,
+ &i.Content,
+ &i.Version,
+ &i.CreatedAt,
+ &i.UpdatedAt,
+ ); err != nil {
+ return nil, err
+ }
+ items = append(items, i)
+ }
+ if err := rows.Close(); err != nil {
+ return nil, err
+ }
+ if err := rows.Err(); err != nil {
+ return nil, err
+ }
+ 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
+}
@@ -1,8 +1,10 @@
DROP TRIGGER IF EXISTS update_sessions_updated_at;
DROP TRIGGER IF EXISTS update_messages_updated_at;
+DROP TRIGGER IF EXISTS update_files_updated_at;
DROP TRIGGER IF EXISTS update_session_message_count_on_delete;
DROP TRIGGER IF EXISTS update_session_message_count_on_insert;
DROP TABLE IF EXISTS sessions;
DROP TABLE IF EXISTS messages;
+DROP TABLE IF EXISTS files;
@@ -18,6 +18,28 @@ UPDATE sessions SET updated_at = strftime('%s', 'now')
WHERE id = new.id;
END;
+-- Files
+CREATE TABLE IF NOT EXISTS files (
+ id TEXT PRIMARY KEY,
+ session_id TEXT NOT NULL,
+ path TEXT NOT NULL,
+ content TEXT NOT NULL,
+ version TEXT NOT NULL,
+ 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
+);
+
+CREATE INDEX IF NOT EXISTS idx_files_session_id ON files (session_id);
+CREATE INDEX IF NOT EXISTS idx_files_path ON files (path);
+
+CREATE TRIGGER IF NOT EXISTS update_files_updated_at
+AFTER UPDATE ON files
+BEGIN
+UPDATE files SET updated_at = strftime('%s', 'now')
+WHERE id = new.id;
+END;
+
-- Messages
CREATE TABLE IF NOT EXISTS messages (
id TEXT PRIMARY KEY,
@@ -8,6 +8,16 @@ import (
"database/sql"
)
+type File struct {
+ ID string `json:"id"`
+ SessionID string `json:"session_id"`
+ Path string `json:"path"`
+ Content string `json:"content"`
+ Version string `json:"version"`
+ CreatedAt int64 `json:"created_at"`
+ UpdatedAt int64 `json:"updated_at"`
+}
+
type Message struct {
ID string `json:"id"`
SessionID string `json:"session_id"`
@@ -9,15 +9,25 @@ import (
)
type Querier interface {
+ CreateFile(ctx context.Context, arg CreateFileParams) (File, error)
CreateMessage(ctx context.Context, arg CreateMessageParams) (Message, error)
CreateSession(ctx context.Context, arg CreateSessionParams) (Session, error)
+ DeleteFile(ctx context.Context, id string) error
DeleteMessage(ctx context.Context, id string) error
DeleteSession(ctx context.Context, id string) error
+ DeleteSessionFiles(ctx context.Context, sessionID string) error
DeleteSessionMessages(ctx context.Context, sessionID string) error
+ GetFile(ctx context.Context, id string) (File, error)
+ GetFileByPathAndSession(ctx context.Context, arg GetFileByPathAndSessionParams) (File, error)
GetMessage(ctx context.Context, id string) (Message, error)
GetSessionByID(ctx context.Context, id string) (Session, error)
+ ListFilesByPath(ctx context.Context, path string) ([]File, error)
+ ListFilesBySession(ctx context.Context, sessionID string) ([]File, error)
+ ListLatestSessionFiles(ctx context.Context, sessionID string) ([]File, error)
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)
}
@@ -0,0 +1,69 @@
+-- name: GetFile :one
+SELECT *
+FROM files
+WHERE id = ? LIMIT 1;
+
+-- name: GetFileByPathAndSession :one
+SELECT *
+FROM files
+WHERE path = ? AND session_id = ? LIMIT 1;
+
+-- name: ListFilesBySession :many
+SELECT *
+FROM files
+WHERE session_id = ?
+ORDER BY created_at ASC;
+
+-- name: ListFilesByPath :many
+SELECT *
+FROM files
+WHERE path = ?
+ORDER BY created_at DESC;
+
+-- name: CreateFile :one
+INSERT INTO files (
+ id,
+ session_id,
+ path,
+ content,
+ version,
+ created_at,
+ updated_at
+) VALUES (
+ ?, ?, ?, ?, ?, strftime('%s', 'now'), strftime('%s', 'now')
+)
+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 = ?;
+
+-- name: DeleteSessionFiles :exec
+DELETE FROM files
+WHERE session_id = ?;
+
+-- name: ListLatestSessionFiles :many
+SELECT f.*
+FROM files f
+INNER JOIN (
+ SELECT path, 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
+WHERE f.session_id = ?
+ORDER BY f.path;
+
+-- name: ListNewFiles :many
+SELECT *
+FROM files
+WHERE is_new = 1
+ORDER BY created_at DESC;
@@ -0,0 +1,206 @@
+package history
+
+import (
+ "context"
+ "fmt"
+ "strconv"
+ "strings"
+
+ "github.com/google/uuid"
+ "github.com/kujtimiihoxha/termai/internal/db"
+ "github.com/kujtimiihoxha/termai/internal/pubsub"
+)
+
+const (
+ InitialVersion = "initial"
+)
+
+type File struct {
+ ID string
+ SessionID string
+ Path string
+ Content string
+ Version string
+ CreatedAt int64
+ UpdatedAt int64
+}
+
+type Service interface {
+ pubsub.Suscriber[File]
+ Create(sessionID, path, content string) (File, error)
+ CreateVersion(sessionID, path, content string) (File, error)
+ Get(id string) (File, error)
+ GetByPathAndSession(path, sessionID string) (File, error)
+ ListBySession(sessionID string) ([]File, error)
+ ListLatestSessionFiles(sessionID string) ([]File, error)
+ Update(file File) (File, error)
+ Delete(id string) error
+ DeleteSessionFiles(sessionID string) error
+}
+
+type service struct {
+ *pubsub.Broker[File]
+ q db.Querier
+ ctx context.Context
+}
+
+func NewService(ctx context.Context, q db.Querier) Service {
+ return &service{
+ Broker: pubsub.NewBroker[File](),
+ q: q,
+ ctx: ctx,
+ }
+}
+
+func (s *service) Create(sessionID, path, content string) (File, error) {
+ return s.createWithVersion(sessionID, path, content, InitialVersion)
+}
+
+func (s *service) CreateVersion(sessionID, path, content string) (File, error) {
+ // Get the latest version for this path
+ files, err := s.q.ListFilesByPath(s.ctx, path)
+ if err != nil {
+ return File{}, err
+ }
+
+ if len(files) == 0 {
+ // No previous versions, create initial
+ return s.Create(sessionID, path, content)
+ }
+
+ // 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)
+ }
+
+ return s.createWithVersion(sessionID, path, content, nextVersion)
+}
+
+func (s *service) createWithVersion(sessionID, path, content, version string) (File, error) {
+ dbFile, err := s.q.CreateFile(s.ctx, db.CreateFileParams{
+ ID: uuid.New().String(),
+ SessionID: sessionID,
+ Path: path,
+ Content: content,
+ Version: version,
+ })
+ if err != nil {
+ return File{}, err
+ }
+ file := s.fromDBItem(dbFile)
+ s.Publish(pubsub.CreatedEvent, file)
+ return file, nil
+}
+
+func (s *service) Get(id string) (File, error) {
+ dbFile, err := s.q.GetFile(s.ctx, id)
+ if err != nil {
+ return File{}, err
+ }
+ return s.fromDBItem(dbFile), nil
+}
+
+func (s *service) GetByPathAndSession(path, sessionID string) (File, error) {
+ dbFile, err := s.q.GetFileByPathAndSession(s.ctx, db.GetFileByPathAndSessionParams{
+ Path: path,
+ SessionID: sessionID,
+ })
+ if err != nil {
+ return File{}, err
+ }
+ return s.fromDBItem(dbFile), nil
+}
+
+func (s *service) ListBySession(sessionID string) ([]File, error) {
+ dbFiles, err := s.q.ListFilesBySession(s.ctx, sessionID)
+ if err != nil {
+ return nil, err
+ }
+ files := make([]File, len(dbFiles))
+ for i, dbFile := range dbFiles {
+ files[i] = s.fromDBItem(dbFile)
+ }
+ return files, nil
+}
+
+func (s *service) ListLatestSessionFiles(sessionID string) ([]File, error) {
+ dbFiles, err := s.q.ListLatestSessionFiles(s.ctx, sessionID)
+ if err != nil {
+ return nil, err
+ }
+ files := make([]File, len(dbFiles))
+ for i, dbFile := range dbFiles {
+ files[i] = s.fromDBItem(dbFile)
+ }
+ return files, nil
+}
+
+func (s *service) Update(file File) (File, error) {
+ dbFile, err := s.q.UpdateFile(s.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(id string) error {
+ file, err := s.Get(id)
+ if err != nil {
+ return err
+ }
+ err = s.q.DeleteFile(s.ctx, id)
+ if err != nil {
+ return err
+ }
+ s.Publish(pubsub.DeletedEvent, file)
+ return nil
+}
+
+func (s *service) DeleteSessionFiles(sessionID string) error {
+ files, err := s.ListBySession(sessionID)
+ if err != nil {
+ return err
+ }
+ for _, file := range files {
+ err = s.Delete(file.ID)
+ if err != nil {
+ return err
+ }
+ }
+ return nil
+}
+
+func (s *service) fromDBItem(item db.File) File {
+ return File{
+ ID: item.ID,
+ SessionID: item.SessionID,
+ Path: item.Path,
+ Content: item.Content,
+ Version: item.Version,
+ CreatedAt: item.CreatedAt,
+ UpdatedAt: item.UpdatedAt,
+ }
+}
+
@@ -198,8 +198,8 @@ func (a appModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
}
return a, util.CmdHandler(repl.SelectedSessionMsg{SessionID: s.ID})
}
- case key.Matches(msg, keys.Logs):
- return a, a.moveToPage(page.LogsPage)
+ // case key.Matches(msg, keys.Logs):
+ // return a, a.moveToPage(page.LogsPage)
case msg.String() == "O":
return a, a.moveToPage(page.ReplPage)
case key.Matches(msg, keys.Help):