dx_forge/context/
annotations.rs

1use anyhow::Result;
2use chrono::{DateTime, Utc};
3use rusqlite::params;
4use serde::{Deserialize, Serialize};
5use std::path::Path;
6use uuid::Uuid;
7
8use crate::storage::Database;
9
10#[derive(Debug, Clone, Serialize, Deserialize)]
11pub struct Annotation {
12    pub id: Uuid,
13    pub file_path: String,
14    pub anchor_id: Option<Uuid>,
15    pub line: usize,
16    pub content: String,
17    pub author: String,
18    pub created_at: DateTime<Utc>,
19    pub is_ai: bool,
20}
21
22impl Annotation {
23    pub fn new(file_path: String, line: usize, content: String, is_ai: bool) -> Self {
24        let author = if is_ai {
25            "AI Agent".to_string()
26        } else {
27            whoami::username()
28        };
29
30        Self {
31            id: Uuid::new_v4(),
32            file_path,
33            anchor_id: None,
34            line,
35            content,
36            author,
37            created_at: Utc::now(),
38            is_ai,
39        }
40    }
41}
42
43pub fn store_annotation(db: &Database, annotation: &Annotation) -> Result<()> {
44    let conn = db.conn.lock();
45
46    conn.execute(
47        "INSERT INTO annotations (id, file_path, anchor_id, line, content, author, created_at, is_ai)
48         VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8)",
49        params![
50            annotation.id.to_string(),
51            annotation.file_path,
52            annotation.anchor_id.map(|id| id.to_string()),
53            annotation.line as i64,
54            annotation.content,
55            annotation.author,
56            annotation.created_at.to_rfc3339(),
57            annotation.is_ai,
58        ],
59    )?;
60
61    Ok(())
62}
63
64pub fn get_annotations(db: &Database, file: &Path, line: Option<usize>) -> Result<Vec<Annotation>> {
65    let conn = db.conn.lock();
66
67    let query = if let Some(l) = line {
68        format!(
69            "SELECT id, file_path, anchor_id, line, content, author, created_at, is_ai
70             FROM annotations
71             WHERE file_path = '{}' AND line = {}
72             ORDER BY created_at DESC",
73            file.display(),
74            l
75        )
76    } else {
77        format!(
78            "SELECT id, file_path, anchor_id, line, content, author, created_at, is_ai
79             FROM annotations
80             WHERE file_path = '{}'
81             ORDER BY created_at DESC",
82            file.display()
83        )
84    };
85
86    let mut stmt = conn.prepare(&query)?;
87    let annotations = stmt.query_map([], |row| {
88        let id: String = row.get(0)?;
89        let file_path: String = row.get(1)?;
90        let anchor_id: Option<String> = row.get(2)?;
91        let line: i64 = row.get(3)?;
92        let content: String = row.get(4)?;
93        let author: String = row.get(5)?;
94        let created_at: String = row.get(6)?;
95        let is_ai: bool = row.get(7)?;
96
97        Ok(Annotation {
98            id: Uuid::parse_str(&id).unwrap(),
99            file_path,
100            anchor_id: anchor_id.as_ref().and_then(|s| Uuid::parse_str(s).ok()),
101            line: line as usize,
102            content,
103            author,
104            created_at: chrono::DateTime::parse_from_rfc3339(&created_at)
105                .unwrap()
106                .into(),
107            is_ai,
108        })
109    })?;
110
111    Ok(annotations.collect::<Result<Vec<_>, _>>()?)
112}