codex_mobile_bridge/storage/
threads.rs1use 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}