Skip to main content

codex_mobile_bridge/storage/
threads.rs

1use std::path::Path;
2
3use rusqlite::{OptionalExtension, params, params_from_iter};
4
5use super::Storage;
6use super::decode::decode_thread_row;
7use crate::bridge_protocol::ThreadSummary;
8use crate::directory::{directory_contains, normalize_absolute_directory};
9
10impl Storage {
11    pub fn upsert_thread_index(&self, thread: &ThreadSummary) -> anyhow::Result<()> {
12        let conn = self.connect()?;
13        conn.execute(
14            "INSERT INTO thread_index (
15                 thread_id, runtime_id, name, note, preview, cwd, status,
16                 model_provider, source, created_at_ms, updated_at_ms, is_loaded, is_active,
17                 archived, raw_json
18             )
19             VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10, ?11, ?12, ?13, ?14, ?15)
20             ON CONFLICT(thread_id) DO UPDATE SET
21                 runtime_id = excluded.runtime_id,
22                 name = excluded.name,
23                 note = COALESCE(excluded.note, thread_index.note),
24                 preview = excluded.preview,
25                 cwd = excluded.cwd,
26                 status = excluded.status,
27                 model_provider = excluded.model_provider,
28                 source = excluded.source,
29                 created_at_ms = excluded.created_at_ms,
30                 updated_at_ms = excluded.updated_at_ms,
31                 is_loaded = excluded.is_loaded,
32                 is_active = excluded.is_active,
33                 archived = excluded.archived,
34                 raw_json = excluded.raw_json",
35            params![
36                thread.id,
37                thread.runtime_id,
38                thread.name,
39                thread.note,
40                thread.preview,
41                thread.cwd,
42                thread.status,
43                thread.model_provider,
44                thread.source,
45                thread.created_at,
46                thread.updated_at,
47                if thread.is_loaded { 1_i64 } else { 0_i64 },
48                if thread.is_active { 1_i64 } else { 0_i64 },
49                if thread.archived { 1_i64 } else { 0_i64 },
50                serde_json::to_string(thread)?
51            ],
52        )?;
53        Ok(())
54    }
55
56    pub fn get_thread_index(&self, thread_id: &str) -> anyhow::Result<Option<ThreadSummary>> {
57        let conn = self.connect()?;
58        let record = conn
59            .query_row(
60                "SELECT raw_json, note, archived FROM thread_index WHERE thread_id = ?1",
61                params![thread_id],
62                |row| {
63                    decode_thread_row(
64                        row.get::<_, String>(0)?,
65                        row.get::<_, Option<String>>(1)?,
66                        row.get::<_, i64>(2)?,
67                    )
68                },
69            )
70            .optional()?;
71        Ok(record)
72    }
73
74    pub fn list_thread_index(
75        &self,
76        directory_prefix: Option<&str>,
77        runtime_id: Option<&str>,
78        archived: Option<bool>,
79        search_term: Option<&str>,
80    ) -> anyhow::Result<Vec<ThreadSummary>> {
81        let conn = self.connect()?;
82        let mut sql = String::from(
83            "SELECT raw_json, note, archived
84             FROM thread_index",
85        );
86        let mut clauses = Vec::new();
87        let mut values = Vec::new();
88
89        if let Some(runtime_id) = runtime_id {
90            clauses.push("runtime_id = ?");
91            values.push(rusqlite::types::Value::from(runtime_id.to_string()));
92        }
93
94        if let Some(archived) = archived {
95            clauses.push("archived = ?");
96            values.push(rusqlite::types::Value::from(if archived {
97                1_i64
98            } else {
99                0_i64
100            }));
101        }
102
103        if let Some(search_term) = search_term.filter(|value| !value.trim().is_empty()) {
104            clauses.push(
105                "(LOWER(COALESCE(name, '')) LIKE ? OR LOWER(preview) LIKE ? OR \
106                 LOWER(cwd) LIKE ? OR LOWER(COALESCE(note, '')) LIKE ?)",
107            );
108            let pattern = format!("%{}%", search_term.trim().to_lowercase());
109            values.push(rusqlite::types::Value::from(pattern.clone()));
110            values.push(rusqlite::types::Value::from(pattern.clone()));
111            values.push(rusqlite::types::Value::from(pattern.clone()));
112            values.push(rusqlite::types::Value::from(pattern));
113        }
114
115        if !clauses.is_empty() {
116            sql.push_str(" WHERE ");
117            sql.push_str(&clauses.join(" AND "));
118        }
119        sql.push_str(" ORDER BY updated_at_ms DESC");
120
121        let mut stmt = conn.prepare(&sql)?;
122        let rows = stmt.query_map(params_from_iter(values), |row| {
123            decode_thread_row(
124                row.get::<_, String>(0)?,
125                row.get::<_, Option<String>>(1)?,
126                row.get::<_, i64>(2)?,
127            )
128        })?;
129
130        let mut threads = rows.collect::<rusqlite::Result<Vec<_>>>()?;
131
132        if let Some(directory_prefix) = directory_prefix {
133            let prefix = normalize_absolute_directory(Path::new(directory_prefix))?;
134            threads.retain(|thread| directory_contains(&prefix, Path::new(&thread.cwd)));
135        }
136
137        Ok(threads)
138    }
139
140    pub fn save_thread_note(&self, thread_id: &str, note: Option<&str>) -> anyhow::Result<()> {
141        let conn = self.connect()?;
142        conn.execute(
143            "UPDATE thread_index
144             SET note = ?2
145             WHERE thread_id = ?1",
146            params![thread_id, note],
147        )?;
148        Ok(())
149    }
150
151    pub fn set_thread_archived(&self, thread_id: &str, archived: bool) -> anyhow::Result<()> {
152        let conn = self.connect()?;
153        conn.execute(
154            "UPDATE thread_index
155             SET archived = ?2
156             WHERE thread_id = ?1",
157            params![thread_id, if archived { 1_i64 } else { 0_i64 }],
158        )?;
159        Ok(())
160    }
161}