diff --git a/codex-rs/memory/src/factory.rs b/codex-rs/memory/src/factory.rs index c44b1a5843a..b9314acc467 100644 --- a/codex-rs/memory/src/factory.rs +++ b/codex-rs/memory/src/factory.rs @@ -35,10 +35,10 @@ pub fn open_repo_store( let be = backend.unwrap_or_else(choose_backend_from_env); Ok(match be { Backend::Jsonl => { - let _path = std::env::var("CODEX_MEMORY_REPO_JSONL") + let path = std::env::var("CODEX_MEMORY_REPO_JSONL") .map(std::path::PathBuf::from) .unwrap_or_else(|_| base.join("memory.jsonl")); - Box::new(JsonlMemoryStore) + Box::new(JsonlMemoryStore::new(path)) } #[cfg(feature = "sqlite")] Backend::Sqlite => { @@ -62,10 +62,10 @@ pub fn open_global_store( let be = backend.unwrap_or_else(choose_backend_from_env); Ok(match be { Backend::Jsonl => { - let _path = std::env::var("CODEX_MEMORY_HOME_JSONL") + let path = std::env::var("CODEX_MEMORY_HOME_JSONL") .map(std::path::PathBuf::from) .unwrap_or_else(|_| base.join("memory.jsonl")); - Box::new(JsonlMemoryStore) + Box::new(JsonlMemoryStore::new(path)) } #[cfg(feature = "sqlite")] Backend::Sqlite => { diff --git a/codex-rs/memory/src/recall.rs b/codex-rs/memory/src/recall.rs index fe74d550e45..51d67651c4b 100644 --- a/codex-rs/memory/src/recall.rs +++ b/codex-rs/memory/src/recall.rs @@ -1,4 +1,7 @@ +use crate::types::Kind; use crate::types::MemoryItem; +use crate::types::Scope; +use crate::types::Status; pub struct RecallContext { pub repo_root: Option, @@ -13,9 +16,29 @@ pub struct RecallContext { } pub fn recall( - _store: &dyn crate::store::MemoryStore, + store: &dyn crate::store::MemoryStore, _prompt: &str, - _ctx: &RecallContext, + ctx: &RecallContext, ) -> anyhow::Result> { - todo!() + // Basic implementation: list active repo-scoped memories and return up to + // `item_cap` items without exceeding `token_cap` characters. + let mut items = store.list(Some(Scope::Repo), Some(Status::Active))?; + // Prefer preferences and facts first; stable sort by updated_at desc. + items.sort_by(|a, b| b.updated_at.cmp(&a.updated_at)); + items.retain(|it| matches!(it.kind, Kind::Pref | Kind::Fact)); + if items.len() > ctx.item_cap { + items.truncate(ctx.item_cap); + } + // Rough token cap approximation: 1 token ~ 1 char. + let mut total = 0usize; + let mut out = Vec::new(); + for it in items { + let len = it.content.len(); + if !out.is_empty() && total + len > ctx.token_cap { + break; + } + total += len; + out.push(it); + } + Ok(out) } diff --git a/codex-rs/memory/src/store/jsonl.rs b/codex-rs/memory/src/store/jsonl.rs index 09976c7de15..32c93adcbaa 100644 --- a/codex-rs/memory/src/store/jsonl.rs +++ b/codex-rs/memory/src/store/jsonl.rs @@ -1,37 +1,151 @@ use super::*; +use std::io::Write; +use std::path::Path; +use std::path::PathBuf; -pub struct JsonlMemoryStore; +/// Simple JSONL-backed memory store used for both durable history and recall. +pub struct JsonlMemoryStore { + path: PathBuf, +} + +impl JsonlMemoryStore { + pub fn new>(path: P) -> Self { + Self { + path: path.as_ref().to_path_buf(), + } + } + + fn read_all(&self) -> anyhow::Result> { + let data = match std::fs::read_to_string(&self.path) { + Ok(s) => s, + Err(e) if e.kind() == std::io::ErrorKind::NotFound => String::new(), + Err(e) => return Err(e.into()), + }; + let mut items = Vec::new(); + for line in data.lines() { + if line.trim().is_empty() { + continue; + } + if let Ok(it) = serde_json::from_str::(line) { + items.push(it); + } + } + Ok(items) + } + + fn write_all(&self, items: &[MemoryItem]) -> anyhow::Result<()> { + let mut out = String::new(); + for it in items { + let line = serde_json::to_string(it)?; + out.push_str(&line); + out.push('\n'); + } + if let Some(dir) = self.path.parent() { + std::fs::create_dir_all(dir)?; + } + std::fs::write(&self.path, out)?; + Ok(()) + } +} impl MemoryStore for JsonlMemoryStore { - fn add(&self, _item: MemoryItem) -> anyhow::Result<()> { - todo!() + fn add(&self, item: MemoryItem) -> anyhow::Result<()> { + if let Some(dir) = self.path.parent() { + std::fs::create_dir_all(dir)?; + } + let mut line = serde_json::to_string(&item)?; + line.push('\n'); + let mut f = std::fs::OpenOptions::new() + .create(true) + .append(true) + .open(&self.path)?; + f.write_all(line.as_bytes())?; + f.flush()?; + Ok(()) } - fn update(&self, _item: &MemoryItem) -> anyhow::Result<()> { - todo!() + + fn update(&self, item: &MemoryItem) -> anyhow::Result<()> { + let mut items = self.read_all()?; + for it in &mut items { + if it.id == item.id { + *it = item.clone(); + } + } + self.write_all(&items) } - fn delete(&self, _id: &str) -> anyhow::Result<()> { - todo!() + + fn delete(&self, id: &str) -> anyhow::Result<()> { + let items = self.read_all()?; + let items: Vec<_> = items.into_iter().filter(|i| i.id != id).collect(); + self.write_all(&items) } - fn get(&self, _id: &str) -> anyhow::Result> { - todo!() + + fn get(&self, id: &str) -> anyhow::Result> { + let items = self.read_all()?; + Ok(items.into_iter().find(|i| i.id == id)) } + fn list( &self, - _scope: Option, - _status: Option, + scope: Option, + status: Option, ) -> anyhow::Result> { - todo!() + let mut items = self.read_all()?; + if let Some(sc) = scope { + items.retain(|i| i.scope == sc); + } + if let Some(st) = status { + items.retain(|i| i.status == st); + } + Ok(items) } - fn archive(&self, _id: &str, _archived: bool) -> anyhow::Result<()> { - todo!() + + fn archive(&self, id: &str, archived: bool) -> anyhow::Result<()> { + let mut items = self.read_all()?; + for it in &mut items { + if it.id == id { + it.status = if archived { + Status::Archived + } else { + Status::Active + }; + } + } + self.write_all(&items) } - fn export(&self, _out: &mut dyn std::io::Write) -> anyhow::Result<()> { - todo!() + + fn export(&self, out: &mut dyn std::io::Write) -> anyhow::Result<()> { + let items = self.read_all()?; + for it in items { + let line = serde_json::to_string(&it)?; + out.write_all(line.as_bytes())?; + out.write_all(b"\n")?; + } + Ok(()) } - fn import(&self, _input: &mut dyn std::io::Read) -> anyhow::Result { - todo!() + + fn import(&self, input: &mut dyn std::io::Read) -> anyhow::Result { + let mut buf = String::new(); + std::io::Read::read_to_string(input, &mut buf)?; + let mut items = self.read_all()?; + let mut count = 0usize; + for line in buf.lines() { + if line.trim().is_empty() { + continue; + } + if let Ok(it) = serde_json::from_str::(line) { + items.push(it); + count += 1; + } + } + self.write_all(&items)?; + Ok(count) } + fn stats(&self) -> anyhow::Result { - todo!() + let items = self.read_all()?; + let total = items.len(); + let active = items.iter().filter(|i| i.status == Status::Active).count(); + Ok(serde_json::json!({"total": total, "active": active})) } } diff --git a/codex-rs/tui/Cargo.toml b/codex-rs/tui/Cargo.toml index 9ab626e47b9..6749b14b8e3 100644 --- a/codex-rs/tui/Cargo.toml +++ b/codex-rs/tui/Cargo.toml @@ -17,7 +17,7 @@ vt100-tests = [] # Gate verbose debug logging inside the TUI implementation. debug-logs = [] # Opt-in: allow using SQLite memory backend from codex-memory. -memory-sqlite = ["codex-memory/sqlite", "codex-memory"] +memory-sqlite = ["codex-memory/sqlite"] [lints] workspace = true @@ -39,7 +39,7 @@ codex-common = { path = "../common", features = [ codex-core = { path = "../core" } codex-file-search = { path = "../file-search" } codex-login = { path = "../login" } -codex-memory = { path = "../memory", optional = true } +codex-memory = { path = "../memory" } codex-ollama = { path = "../ollama" } codex-protocol = { path = "../protocol" } color-eyre = "0.6.3" diff --git a/codex-rs/tui/src/lib.rs b/codex-rs/tui/src/lib.rs index 71af637a9e7..8dd8c377511 100644 --- a/codex-rs/tui/src/lib.rs +++ b/codex-rs/tui/src/lib.rs @@ -45,7 +45,8 @@ pub mod insert_history; pub mod live_wrap; mod markdown; mod markdown_stream; -mod memory; +pub mod memories_panel; +pub mod memory; pub mod onboarding; mod pager_overlay; mod render; diff --git a/codex-rs/tui/src/memories_panel.rs b/codex-rs/tui/src/memories_panel.rs new file mode 100644 index 00000000000..0ccd6900750 --- /dev/null +++ b/codex-rs/tui/src/memories_panel.rs @@ -0,0 +1,90 @@ +use chrono::Utc; +use ratatui::buffer::Buffer; +use ratatui::layout::Rect; +use ratatui::text::Line; +use ratatui::widgets::Paragraph; +use ratatui::widgets::{Widget, WidgetRef}; +use uuid::Uuid; + +use codex_memory::factory; +use codex_memory::types::Counters; +use codex_memory::types::Kind; +use codex_memory::types::MemoryItem; +use codex_memory::types::RelevanceHints; +use codex_memory::types::Scope; +use codex_memory::types::Status; + +/// Simple panel showing stored memories and exposing minimal CRUD ops. +pub struct MemoriesPanel { + repo_root: std::path::PathBuf, + items: Vec, +} + +impl MemoriesPanel { + pub fn new(repo_root: std::path::PathBuf) -> anyhow::Result { + let mut panel = Self { + repo_root, + items: Vec::new(), + }; + panel.refresh()?; + Ok(panel) + } + + /// Reload items from the repo store. + pub fn refresh(&mut self) -> anyhow::Result<()> { + let store = factory::open_repo_store(&self.repo_root, None)?; + self.items = store.list(Some(Scope::Repo), Some(Status::Active))?; + Ok(()) + } + + /// Add a new preference memory entry. + pub fn add_pref(&mut self, text: &str) -> anyhow::Result<()> { + let ts = Utc::now().to_rfc3339_opts(chrono::SecondsFormat::Millis, true); + let item = MemoryItem { + id: Uuid::new_v4().to_string(), + created_at: ts.clone(), + updated_at: ts, + schema_version: 1, + source: "codex-tui".to_string(), + scope: Scope::Repo, + status: Status::Active, + kind: Kind::Pref, + content: text.to_string(), + tags: vec!["pref".to_string()], + relevance_hints: RelevanceHints { + files: vec![], + crates: vec![], + languages: vec![], + commands: vec![], + }, + counters: Counters { + seen_count: 0, + used_count: 0, + last_used_at: None, + }, + expiry: None, + }; + let store = factory::open_repo_store(&self.repo_root, None)?; + store.add(item)?; + self.refresh() + } + + /// Delete a memory item by id. + pub fn delete(&mut self, id: &str) -> anyhow::Result<()> { + let store = factory::open_repo_store(&self.repo_root, None)?; + store.delete(id)?; + self.refresh() + } +} + +impl WidgetRef for &MemoriesPanel { + fn render_ref(&self, area: Rect, buf: &mut Buffer) { + let mut lines: Vec> = Vec::with_capacity(self.items.len() + 1); + lines.push(Line::raw("Memories:")); + for it in &self.items { + lines.push(Line::raw(format!("- {}", it.content))); + } + let para = Paragraph::new(lines); + para.render(area, buf); + } +} diff --git a/codex-rs/tui/src/memory.rs b/codex-rs/tui/src/memory.rs index fb8aac8bee7..c4b17b6824b 100644 --- a/codex-rs/tui/src/memory.rs +++ b/codex-rs/tui/src/memory.rs @@ -9,12 +9,17 @@ use std::path::PathBuf; use std::time::Duration; use uuid::Uuid; -#[cfg(feature = "memory-sqlite")] use codex_memory::factory; -#[cfg(feature = "memory-sqlite")] -use codex_memory::types::{Counters, Kind, MemoryItem, RelevanceHints, Scope, Status}; +use codex_memory::recall::RecallContext; +use codex_memory::recall::{self}; +use codex_memory::types::Counters; +use codex_memory::types::Kind; +use codex_memory::types::MemoryItem; +use codex_memory::types::RelevanceHints; +use codex_memory::types::Scope; +use codex_memory::types::Status; -pub(crate) struct MemoryLogger { +pub struct MemoryLogger { repo_root: PathBuf, memory_dir: PathBuf, memory_file: PathBuf, @@ -197,43 +202,33 @@ impl MemoryLogger { // Build a short preamble string from durable memory items (prefs/summaries). pub fn build_durable_preamble(&self, max_len: usize) -> Option { - let path = self.memory_file.as_path(); - let Ok(file) = std::fs::File::open(path) else { + let store = factory::open_repo_store(&self.repo_root, None).ok()?; + let ctx = RecallContext { + repo_root: Some(self.repo_root.clone()), + dir: None, + current_file: None, + crate_name: None, + language: None, + command: None, + now_rfc3339: Utc::now().to_rfc3339_opts(chrono::SecondsFormat::Millis, true), + item_cap: 16, + token_cap: max_len * 2, + }; + let Ok(items) = recall::recall(store.as_ref(), "", &ctx) else { return None; }; - let reader = std::io::BufReader::new(file); let mut prefs: Vec<(String, Vec)> = Vec::new(); let mut summaries: Vec<(String, Vec)> = Vec::new(); - for line in std::io::BufRead::lines(reader).flatten() { - if let Ok(v) = serde_json::from_str::(&line) { - let t = v.get("type").and_then(|x| x.as_str()).unwrap_or(""); - if t == "pref" || t == "summary" { - let c = v - .get("content") - .and_then(|x| x.as_str()) - .unwrap_or("") - .to_string(); - let tags: Vec = v - .get("tags") - .and_then(|x| x.as_array()) - .map(|arr| { - arr.iter() - .filter_map(|t| t.as_str().map(|s| s.to_string())) - .collect() - }) - .unwrap_or_default(); - if t == "pref" { - prefs.push((c, tags)); - } else { - summaries.push((c, tags)); - } - } + for it in items { + match it.kind { + Kind::Pref => prefs.push((it.content, it.tags)), + Kind::Fact => summaries.push((it.content, it.tags)), + _ => {} } } if prefs.is_empty() && summaries.is_empty() { return None; } - // Dedupe/merge by content prefix and tags; cap counts let dedupe = |items: Vec<(String, Vec)>, cap: usize| -> Vec { use std::collections::BTreeMap; let mut map: BTreeMap, usize)> = BTreeMap::new(); @@ -304,8 +299,17 @@ impl MemoryLogger { kind: Kind::Pref, content: text.to_string(), tags: vec!["pref".to_string()], - relevance_hints: RelevanceHints { files: vec![], crates: vec![], languages: vec![], commands: vec![] }, - counters: Counters { seen_count: 0, used_count: 0, last_used_at: None }, + relevance_hints: RelevanceHints { + files: vec![], + crates: vec![], + languages: vec![], + commands: vec![], + }, + counters: Counters { + seen_count: 0, + used_count: 0, + last_used_at: None, + }, expiry: None, }; let store = factory::open_repo_store(&self.repo_root, None)?; @@ -335,14 +339,25 @@ impl MemoryLogger { if sqlite_enabled() { #[cfg(feature = "memory-sqlite")] { - let Ok(store) = factory::open_repo_store(&self.repo_root, None) else { return vec![]; }; - let Ok(mut items) = store.list(Some(Scope::Repo), Some(Status::Active)) else { return vec![]; }; + let Ok(store) = factory::open_repo_store(&self.repo_root, None) else { + return vec![]; + }; + let Ok(mut items) = store.list(Some(Scope::Repo), Some(Status::Active)) else { + return vec![]; + }; // Only preferences and summaries (facts) items.retain(|i| matches!(i.kind, Kind::Pref | Kind::Fact)); items.truncate(limit); return items .into_iter() - .map(|i| DurableItem { id: i.id, r#type: match i.kind { Kind::Pref => "pref".to_string(), _ => "summary".to_string() }, content: i.content }) + .map(|i| DurableItem { + id: i.id, + r#type: match i.kind { + Kind::Pref => "pref".to_string(), + _ => "summary".to_string(), + }, + content: i.content, + }) .collect(); } } @@ -388,13 +403,27 @@ impl MemoryLogger { #[cfg(feature = "memory-sqlite")] { let t = tag.to_ascii_lowercase(); - let Ok(store) = factory::open_repo_store(&self.repo_root, None) else { return vec![]; }; - let Ok(mut items) = store.list(Some(Scope::Repo), Some(Status::Active)) else { return vec![]; }; - items.retain(|i| i.tags.iter().any(|x| x.eq_ignore_ascii_case(&t)) && matches!(i.kind, Kind::Pref | Kind::Fact)); + let Ok(store) = factory::open_repo_store(&self.repo_root, None) else { + return vec![]; + }; + let Ok(mut items) = store.list(Some(Scope::Repo), Some(Status::Active)) else { + return vec![]; + }; + items.retain(|i| { + i.tags.iter().any(|x| x.eq_ignore_ascii_case(&t)) + && matches!(i.kind, Kind::Pref | Kind::Fact) + }); items.truncate(limit); return items .into_iter() - .map(|i| DurableItem { id: i.id, r#type: match i.kind { Kind::Pref => "pref".to_string(), _ => "summary".to_string() }, content: i.content }) + .map(|i| DurableItem { + id: i.id, + r#type: match i.kind { + Kind::Pref => "pref".to_string(), + _ => "summary".to_string(), + }, + content: i.content, + }) .collect(); } } @@ -440,16 +469,18 @@ impl MemoryLogger { return false; } if let Ok(store) = factory::open_repo_store(&self.repo_root, None) - && let Ok(items) = store.list(Some(Scope::Repo), None) { - let mut changed = false; - for it in items { - if (matches!(it.kind, Kind::Pref | Kind::Fact)) && it.id.starts_with(prefix) { - let _ = store.delete(&it.id); - changed = true; - } + && let Ok(items) = store.list(Some(Scope::Repo), None) + { + let mut changed = false; + for it in items { + if (matches!(it.kind, Kind::Pref | Kind::Fact)) && it.id.starts_with(prefix) + { + let _ = store.delete(&it.id); + changed = true; } - return changed; } + return changed; + } return false; } } @@ -480,9 +511,9 @@ impl MemoryLogger { .write(true) .truncate(true) .open(&self.memory_file) - { - let _ = writeln!(f, "{}", out.join("\n")); - } + { + let _ = writeln!(f, "{}", out.join("\n")); + } changed } } @@ -561,21 +592,43 @@ mod tests { fn preamble_dedupes_merges_and_caps() { let dir = tempdir().unwrap(); let repo = dir.path().to_path_buf(); - let memdir = repo.join(".codex").join("memory"); - fs::create_dir_all(&memdir).unwrap(); - let memfile = memdir.join("memory.jsonl"); - - // Create a mix of durable prefs and summaries with duplicate content and tags. - let lines = vec![ - json!({"id":"1","ts":"2025-01-01T00:00:00.000Z","repo":repo,"type":"pref","content":"prefer ruff","tags":["python","style"],"files":[],"session_id":null,"source":"test","metadata":{}}), - json!({"id":"2","ts":"2025-01-01T00:00:01.000Z","repo":repo,"type":"pref","content":"Prefer Ruff","tags":["style"],"files":[],"session_id":null,"source":"test","metadata":{}}), - json!({"id":"3","ts":"2025-01-01T00:00:02.000Z","repo":repo,"type":"summary","content":"uses pytest","tags":["python"],"files":[],"session_id":null,"source":"test","metadata":{}}), - json!({"id":"4","ts":"2025-01-01T00:00:03.000Z","repo":repo,"type":"summary","content":"Uses PyTest","tags":["ci"],"files":[],"session_id":null,"source":"test","metadata":{}}), - ]; - write_jsonl(&memfile, &lines); + fs::create_dir_all(repo.join(".codex").join("memory")).unwrap(); + let store = factory::open_repo_store(&repo, None).unwrap(); + let ts = "2025-01-01T00:00:00.000Z".to_string(); + let item = MemoryItem { + id: "1".into(), + created_at: ts.clone(), + updated_at: ts.clone(), + schema_version: 1, + source: "test".into(), + scope: Scope::Repo, + status: Status::Active, + kind: Kind::Pref, + content: "prefer ruff".into(), + tags: vec!["python".into(), "style".into()], + relevance_hints: RelevanceHints { files: vec![], crates: vec![], languages: vec![], commands: vec![] }, + counters: Counters { seen_count: 0, used_count: 0, last_used_at: None }, + expiry: None, + }; + store.add(item).unwrap(); + let mut item2 = store.get("1").unwrap().unwrap(); + item2.id = "2".into(); + item2.content = "Prefer Ruff".into(); + item2.tags = vec!["style".into()]; + store.add(item2).unwrap(); + let mut fact = store.get("1").unwrap().unwrap(); + fact.id = "3".into(); + fact.kind = Kind::Fact; + fact.content = "uses pytest".into(); + fact.tags = vec!["python".into()]; + store.add(fact).unwrap(); + let mut fact2 = store.get("3").unwrap().unwrap(); + fact2.id = "4".into(); + fact2.content = "Uses PyTest".into(); + fact2.tags = vec!["ci".into()]; + store.add(fact2).unwrap(); let mut logger = MemoryLogger::new(repo.clone()); - // Ensure we read from the temp repo logger.session_id = Some("test".into()); let pre = logger.build_durable_preamble(512).expect("preamble"); diff --git a/codex-rs/tui/tests/suite/memories_panel.rs b/codex-rs/tui/tests/suite/memories_panel.rs new file mode 100644 index 00000000000..addf7fc1918 --- /dev/null +++ b/codex-rs/tui/tests/suite/memories_panel.rs @@ -0,0 +1,55 @@ +use codex_tui::memories_panel::MemoriesPanel; +use codex_tui::memory::MemoryLogger; +use ratatui::backend::TestBackend; +use ratatui::widgets::WidgetRef; +use ratatui::Terminal; +use tempfile::tempdir; + +#[test] +fn panel_renders() { + let dir = tempdir().unwrap(); + std::fs::create_dir(dir.path().join(".codex")).unwrap(); + let mut panel = MemoriesPanel::new(dir.path().to_path_buf()).unwrap(); + panel.add_pref("Use ripgrep for search").unwrap(); + panel.add_pref("Avoid force pushes").unwrap(); + panel.refresh().unwrap(); + + let mut terminal = Terminal::new(TestBackend::new(40, 6)).unwrap(); + terminal + .draw(|f| (&panel).render_ref(f.area(), f.buffer_mut())) + .unwrap(); + insta::assert_snapshot!(terminal.backend()); +} + +#[test] +fn preamble_preview() { + let dir = tempdir().unwrap(); + std::fs::create_dir(dir.path().join(".codex")).unwrap(); + let repo = dir.path().to_path_buf(); + let store = codex_memory::factory::open_repo_store(&repo, None).unwrap(); + let ts = "2025-01-01T00:00:00.000Z".to_string(); + let pref = codex_memory::types::MemoryItem { + id: "1".into(), + created_at: ts.clone(), + updated_at: ts.clone(), + schema_version: 1, + source: "test".into(), + scope: codex_memory::types::Scope::Repo, + status: codex_memory::types::Status::Active, + kind: codex_memory::types::Kind::Pref, + content: "Respect editorconfig".into(), + tags: vec![], + relevance_hints: codex_memory::types::RelevanceHints { files: vec![], crates: vec![], languages: vec![], commands: vec![] }, + counters: codex_memory::types::Counters { seen_count: 0, used_count: 0, last_used_at: None }, + expiry: None, + }; + store.add(pref).unwrap(); + let mut fact = store.get("1").unwrap().unwrap(); + fact.id = "2".into(); + fact.kind = codex_memory::types::Kind::Fact; + fact.content = "Tests use cargo test".into(); + store.add(fact).unwrap(); + let logger = MemoryLogger::new(repo); + let pre = logger.build_durable_preamble(512).unwrap(); + insta::assert_snapshot!(pre); +} diff --git a/codex-rs/tui/tests/suite/mod.rs b/codex-rs/tui/tests/suite/mod.rs index d120546c71c..987025f2dfa 100644 --- a/codex-rs/tui/tests/suite/mod.rs +++ b/codex-rs/tui/tests/suite/mod.rs @@ -1,4 +1,5 @@ // Aggregates all former standalone integration tests as modules. +mod memories_panel; mod status_indicator; mod vt100_history; mod vt100_live_commit; diff --git a/codex-rs/tui/tests/suite/snapshots/all__suite__memories_panel__panel_renders.snap b/codex-rs/tui/tests/suite/snapshots/all__suite__memories_panel__panel_renders.snap new file mode 100644 index 00000000000..54110769dfe --- /dev/null +++ b/codex-rs/tui/tests/suite/snapshots/all__suite__memories_panel__panel_renders.snap @@ -0,0 +1,10 @@ +--- +source: tui/tests/suite/memories_panel.rs +expression: terminal.backend() +--- +"Memories: " +"- Use ripgrep for search " +"- Avoid force pushes " +" " +" " +" " diff --git a/codex-rs/tui/tests/suite/snapshots/all__suite__memories_panel__preamble_preview.snap b/codex-rs/tui/tests/suite/snapshots/all__suite__memories_panel__preamble_preview.snap new file mode 100644 index 00000000000..483e9555343 --- /dev/null +++ b/codex-rs/tui/tests/suite/snapshots/all__suite__memories_panel__preamble_preview.snap @@ -0,0 +1,12 @@ +--- +source: tui/tests/suite/memories_panel.rs +assertion_line: 54 +expression: pre +--- +Context: The following project memory may be helpful. +Project preferences: +- respect editorconfig + +Project facts: +- tests use cargo test +Please follow these preferences and consider these facts.