Refactor API integration and enhance command handling
- Removed unused modules and updated module paths for clarity. - Added autocomplete functionality for command input in InputState. - Updated AppStateRest to include a method for checking if a turn is in flight. - Refactored subagent engine to use new API client structure. - Changed default provider from "openrouter" to "zen" with updated API keys and models. - Implemented tests for memory management and edit log functionalities. - Enhanced error handling in API requests and improved response parsing. - Updated UI components to reflect new API provider and status indicators.
This commit is contained in:
@@ -27,23 +27,23 @@ pub struct ModelRole {
|
||||
impl Default for AppConfig {
|
||||
fn default() -> Self {
|
||||
let mut providers = HashMap::new();
|
||||
providers.insert("openrouter".to_string(), ProviderConfig {
|
||||
api_base: "https://openrouter.ai/api/v1".to_string(),
|
||||
api_key_env: Some("OPENROUTER_API_KEY".to_string()),
|
||||
default_model: Some("anthropic/claude-opus-4-8".to_string()),
|
||||
providers.insert("zen".to_string(), ProviderConfig {
|
||||
api_base: "https://opencode.ai/zen/v1".to_string(),
|
||||
api_key_env: Some("API_KEY".to_string()),
|
||||
default_model: Some("deepseek-v4-flash-free".to_string()),
|
||||
});
|
||||
let mut model_roles = HashMap::new();
|
||||
model_roles.insert("default".to_string(), ModelRole {
|
||||
provider: "openrouter".to_string(),
|
||||
model: "anthropic/claude-opus-4-8".to_string(),
|
||||
provider: "zen".to_string(),
|
||||
model: "deepseek-v4-flash-free".to_string(),
|
||||
max_tokens: Some(8192),
|
||||
temperature: Some(0.7),
|
||||
});
|
||||
AppConfig {
|
||||
providers,
|
||||
model_roles,
|
||||
default_provider: "openrouter".to_string(),
|
||||
default_model: "anthropic/claude-opus-4-8".to_string(),
|
||||
default_provider: "zen".to_string(),
|
||||
default_model: "deepseek-v4-flash-free".to_string(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -62,3 +62,76 @@ impl EditLog {
|
||||
self.entries.len()
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_editlog_new_empty() {
|
||||
let dir = std::env::temp_dir().join("editlog_test");
|
||||
let _ = std::fs::create_dir_all(&dir);
|
||||
let log = EditLog::new(&dir);
|
||||
assert_eq!(log.len(), 0);
|
||||
assert_eq!(log.recent(5).len(), 0);
|
||||
let _ = std::fs::remove_dir_all(&dir);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_editlog_append_and_reload() {
|
||||
let dir = std::env::temp_dir().join("editlog_append_test");
|
||||
let _ = std::fs::create_dir_all(&dir);
|
||||
let mut log = EditLog::new(&dir);
|
||||
let entry = EditLogEntry {
|
||||
ts: 1,
|
||||
tool: "write".to_string(),
|
||||
path: "test.txt".to_string(),
|
||||
reason: "test reason".to_string(),
|
||||
content_sha256: "abc123".to_string(),
|
||||
bytes_delta: 42,
|
||||
origin: "main".to_string(),
|
||||
session_id: "sess-1".to_string(),
|
||||
};
|
||||
log.append(entry.clone()).unwrap();
|
||||
assert_eq!(log.len(), 1);
|
||||
|
||||
let loaded = EditLog::load(&log.path).unwrap();
|
||||
assert_eq!(loaded.len(), 1);
|
||||
assert_eq!(loaded.entries[0].reason, "test reason");
|
||||
assert_eq!(loaded.entries[0].tool, "write");
|
||||
assert_eq!(loaded.entries[0].path, "test.txt");
|
||||
|
||||
let recent = log.recent(1);
|
||||
assert_eq!(recent.len(), 1);
|
||||
assert_eq!(recent[0].bytes_delta, 42);
|
||||
|
||||
let empty = log.recent(0);
|
||||
assert_eq!(empty.len(), 0);
|
||||
let _ = std::fs::remove_dir_all(&dir);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_editlog_multiple_entries() {
|
||||
let dir = std::env::temp_dir().join("editlog_multiple_test");
|
||||
let _ = std::fs::create_dir_all(&dir);
|
||||
let mut log = EditLog::new(&dir);
|
||||
for i in 0..5 {
|
||||
log.append(EditLogEntry {
|
||||
ts: i,
|
||||
tool: "edit".to_string(),
|
||||
path: format!("file{}.txt", i),
|
||||
reason: format!("reason {}", i),
|
||||
content_sha256: "hash".to_string(),
|
||||
bytes_delta: 10 + i,
|
||||
origin: "main".to_string(),
|
||||
session_id: "sess-1".to_string(),
|
||||
}).unwrap();
|
||||
}
|
||||
assert_eq!(log.len(), 5);
|
||||
let recent = log.recent(3);
|
||||
assert_eq!(recent.len(), 3);
|
||||
assert_eq!(recent[0].reason, "reason 2");
|
||||
assert_eq!(recent[2].reason, "reason 4");
|
||||
let _ = std::fs::remove_dir_all(&dir);
|
||||
}
|
||||
}
|
||||
|
||||
+170
-1
@@ -74,7 +74,8 @@ impl Memory {
|
||||
}
|
||||
|
||||
pub fn parse(content: &str) -> std::io::Result<Self> {
|
||||
let parts: Vec<&str> = content.splitn(2, "---\n").collect();
|
||||
let content = content.strip_prefix("---\n").unwrap_or(content);
|
||||
let parts: Vec<&str> = content.splitn(2, "\n---\n").collect();
|
||||
if parts.len() < 2 {
|
||||
return Err(std::io::Error::new(std::io::ErrorKind::InvalidData, "missing frontmatter"));
|
||||
}
|
||||
@@ -218,6 +219,174 @@ pub fn auto_create_retrospective(session_dir: &Path, session: &Session) -> std::
|
||||
Ok(Some(retrospective))
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_slugify_empty() {
|
||||
assert_eq!(Memory::slugify(""), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_slugify_basic() {
|
||||
assert_eq!(Memory::slugify("Hello World"), Some("hello-world".to_string()));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_slugify_special_chars() {
|
||||
assert_eq!(Memory::slugify("Use & Avoid! @#$"), Some("use-avoid".to_string()));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_slugify_too_long() {
|
||||
let long = "a".repeat(100);
|
||||
assert_eq!(Memory::slugify(&long), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_slugify_numeric() {
|
||||
assert_eq!(Memory::slugify("123"), Some("123".to_string()));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_memory_parse_basic() {
|
||||
let md = "---\nname: test-memory\ndescription: A test memory\nkind: lesson\ncreated_at: 1000\nupdated_at: 2000\n---\n\nThis is the body.";
|
||||
let mem = Memory::parse(md).unwrap();
|
||||
assert_eq!(mem.name, "test-memory");
|
||||
assert_eq!(mem.description, "A test memory");
|
||||
assert_eq!(mem.kind, "lesson");
|
||||
assert_eq!(mem.created_at, 1000);
|
||||
assert_eq!(mem.updated_at, 2000);
|
||||
assert_eq!(mem.content, "This is the body.");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_memory_parse_with_optional_fields() {
|
||||
let md = "---\nname: full-memory\ndescription: Full fields\ntype: reference\ncreated_at: 100\nupdated_at: 200\nlifecycle: active\nscope: project\n---\n\nBody content here.";
|
||||
let mem = Memory::parse(md).unwrap();
|
||||
assert_eq!(mem.name, "full-memory");
|
||||
assert_eq!(mem.lifecycle, "active");
|
||||
assert_eq!(mem.scope, Some("project".to_string()));
|
||||
assert_eq!(mem.content, "Body content here.");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_memory_parse_missing_frontmatter() {
|
||||
let md = "No frontmatter here";
|
||||
assert!(Memory::parse(md).is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_memory_write_and_read() {
|
||||
let dir = std::env::temp_dir().join("memory_test_write_read");
|
||||
let _ = std::fs::create_dir_all(&dir);
|
||||
let mem = Memory {
|
||||
name: "my-test".to_string(),
|
||||
description: "Test".to_string(),
|
||||
content: "Some content".to_string(),
|
||||
kind: "reference".to_string(),
|
||||
created_at: 42,
|
||||
updated_at: 43,
|
||||
outcome: None,
|
||||
lifecycle: "new".to_string(),
|
||||
scope: None,
|
||||
before_snippet: None,
|
||||
after_snippet: None,
|
||||
provenances: vec![],
|
||||
};
|
||||
mem.write(&dir).unwrap();
|
||||
let read = Memory::read(&dir, "my-test").unwrap();
|
||||
assert_eq!(read.name, "my-test");
|
||||
assert_eq!(read.content, "Some content");
|
||||
assert_eq!(read.created_at, 42);
|
||||
let _ = std::fs::remove_dir_all(&dir);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_memory_list() {
|
||||
let dir = std::env::temp_dir().join("memory_test_list");
|
||||
let _ = std::fs::create_dir_all(&dir);
|
||||
let mem = Memory {
|
||||
name: "alpha".to_string(),
|
||||
description: "A".to_string(),
|
||||
content: "a".to_string(),
|
||||
kind: "lesson".to_string(),
|
||||
created_at: 1,
|
||||
updated_at: 1,
|
||||
outcome: None,
|
||||
lifecycle: "new".to_string(),
|
||||
scope: None,
|
||||
before_snippet: None,
|
||||
after_snippet: None,
|
||||
provenances: vec![],
|
||||
};
|
||||
mem.write(&dir).unwrap();
|
||||
let names = Memory::list(&dir);
|
||||
assert!(names.contains(&"alpha".to_string()), "list should contain 'alpha', got: {:?}", names);
|
||||
let _ = std::fs::remove_dir_all(&dir);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_memory_remove() {
|
||||
let dir = std::env::temp_dir().join("memory_test_remove");
|
||||
let _ = std::fs::create_dir_all(&dir);
|
||||
let mem = Memory {
|
||||
name: "remove-me".to_string(),
|
||||
description: "R".to_string(),
|
||||
content: "r".to_string(),
|
||||
kind: "lesson".to_string(),
|
||||
created_at: 1,
|
||||
updated_at: 1,
|
||||
outcome: None,
|
||||
lifecycle: "new".to_string(),
|
||||
scope: None,
|
||||
before_snippet: None,
|
||||
after_snippet: None,
|
||||
provenances: vec![],
|
||||
};
|
||||
mem.write(&dir).unwrap();
|
||||
assert!(Memory::read(&dir, "remove-me").is_ok());
|
||||
Memory::remove(&dir, "remove-me").unwrap();
|
||||
assert!(Memory::read(&dir, "remove-me").is_err());
|
||||
let _ = std::fs::remove_dir_all(&dir);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_export_import_lessons() {
|
||||
let dir = std::env::temp_dir().join("memory_test_export");
|
||||
let _ = std::fs::create_dir_all(&dir);
|
||||
let mem = Memory {
|
||||
name: "export-me".to_string(),
|
||||
description: "Exported".to_string(),
|
||||
content: "content".to_string(),
|
||||
kind: "lesson".to_string(),
|
||||
created_at: 10,
|
||||
updated_at: 10,
|
||||
outcome: None,
|
||||
lifecycle: "active".to_string(),
|
||||
scope: Some("project".to_string()),
|
||||
before_snippet: None,
|
||||
after_snippet: None,
|
||||
provenances: vec![],
|
||||
};
|
||||
mem.write(&dir).unwrap();
|
||||
|
||||
let export_path = std::env::temp_dir().join("memory_test_export_lessons.json");
|
||||
export_lessons(&dir, &export_path).unwrap();
|
||||
assert!(export_path.exists());
|
||||
|
||||
let dest_dir = std::env::temp_dir().join("memory_test_import_dest");
|
||||
let _ = std::fs::create_dir_all(&dest_dir);
|
||||
let imported = import_lessons(&dest_dir, &export_path).unwrap();
|
||||
assert_eq!(imported, 1);
|
||||
assert!(Memory::read(&dest_dir, "export-me").is_ok());
|
||||
let _ = std::fs::remove_dir_all(&dir);
|
||||
let _ = std::fs::remove_dir_all(&dest_dir);
|
||||
let _ = std::fs::remove_file(&export_path);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn create_retrospective(session_dir: &Path, session: &Session, lessons: &[Memory]) -> std::io::Result<Memory> {
|
||||
let now = chrono::Utc::now().timestamp_millis();
|
||||
let lessons_content: String = lessons.iter()
|
||||
|
||||
@@ -45,8 +45,8 @@ impl Default for Settings {
|
||||
fn default() -> Self {
|
||||
Settings {
|
||||
internet_mode: InternetMode::Off,
|
||||
provider: "openrouter".to_string(),
|
||||
model: "anthropic/claude-opus-4-8".to_string(),
|
||||
provider: "zen".to_string(),
|
||||
model: "deepseek-v4-flash-free".to_string(),
|
||||
api_key: None,
|
||||
max_tokens: 8192,
|
||||
temperature: 0.7,
|
||||
|
||||
Reference in New Issue
Block a user