Refactor view modules for improved readability and consistency
- Updated markdown rendering logic to use more concise methods for obtaining vector lengths. - Changed review status display to use the correct flag from settings. - Cleaned up sidebar rendering code for better formatting and readability. - Enhanced status bar rendering with improved string formatting and consistent style application. - Refined workflow panel rendering, ensuring consistent style usage and improved readability. - Added architecture overview and detailed documentation for backend, data, dependencies, and frontend structures.
This commit is contained in:
@@ -1,5 +1,4 @@
|
||||
//! Hardcoded built-in subagent definitions (coder, reviewer, researcher, planner).
|
||||
|
||||
use crate::app::subagent::spawn::AgentDefinition;
|
||||
|
||||
/// Build the fixed list of built-in agent definitions shipped with zesdex.
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
//! Load, save, and remove user-defined agent definitions stored globally
|
||||
//! (under the store's `agents/` directory), independent of any session.
|
||||
|
||||
use crate::app::subagent::spawn::AgentDefinition;
|
||||
|
||||
/// Load all globally-registered agent definitions from disk.
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
//! Agent definition sources: built-in defaults, global (user-wide), and
|
||||
//! per-session overrides.
|
||||
|
||||
pub mod builtin;
|
||||
pub mod global;
|
||||
pub mod session;
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
//! Load, save, add, and remove agent definitions scoped to a single
|
||||
//! session (`<session_dir>/agents.json`).
|
||||
|
||||
use std::path::Path;
|
||||
use crate::app::subagent::spawn::AgentDefinition;
|
||||
|
||||
|
||||
+43
-31
@@ -1,6 +1,5 @@
|
||||
//! Application-level configuration: LLM providers, model roles, and defaults,
|
||||
//! persisted to `app_config.json` in the store directory.
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::collections::HashMap;
|
||||
|
||||
@@ -38,26 +37,35 @@ pub struct ModelRole {
|
||||
impl Default for AppConfig {
|
||||
fn default() -> Self {
|
||||
let mut providers = HashMap::new();
|
||||
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()),
|
||||
default_api_key: None,
|
||||
});
|
||||
providers.insert("router".to_string(), ProviderConfig {
|
||||
api_base: "https://9router.asepharyana.my.id/v1".to_string(),
|
||||
api_key_env: Some("ROUTER_API_KEY".to_string()),
|
||||
default_model: Some("claude-opus-4-8".to_string()),
|
||||
default_api_key: None,
|
||||
});
|
||||
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()),
|
||||
default_api_key: None,
|
||||
},
|
||||
);
|
||||
providers.insert(
|
||||
"router".to_string(),
|
||||
ProviderConfig {
|
||||
api_base: "https://9router.asepharyana.my.id/v1".to_string(),
|
||||
api_key_env: Some("ROUTER_API_KEY".to_string()),
|
||||
default_model: Some("claude-opus-4-8".to_string()),
|
||||
default_api_key: None,
|
||||
},
|
||||
);
|
||||
let mut model_roles = HashMap::new();
|
||||
model_roles.insert("default".to_string(), ModelRole {
|
||||
provider: "zen".to_string(),
|
||||
model: "deepseek-v4-flash-free".to_string(),
|
||||
max_tokens: None,
|
||||
context_window: None,
|
||||
temperature: Some(0.7),
|
||||
});
|
||||
model_roles.insert(
|
||||
"default".to_string(),
|
||||
ModelRole {
|
||||
provider: "zen".to_string(),
|
||||
model: "deepseek-v4-flash-free".to_string(),
|
||||
max_tokens: None,
|
||||
context_window: None,
|
||||
temperature: Some(0.7),
|
||||
},
|
||||
);
|
||||
AppConfig {
|
||||
providers,
|
||||
model_roles,
|
||||
@@ -89,7 +97,8 @@ impl AppConfig {
|
||||
Err(e) => {
|
||||
tracing::warn!(
|
||||
"warning: failed to parse config file '{}': {}. Loading defaults.",
|
||||
path.display(), e
|
||||
path.display(),
|
||||
e
|
||||
);
|
||||
Self::default()
|
||||
}
|
||||
@@ -103,7 +112,9 @@ impl AppConfig {
|
||||
}
|
||||
// Auto-detect provider from ~/.claude/settings.json
|
||||
if let Some(claude_provider) = detect_claude_settings_provider() {
|
||||
cfg.providers.entry("claude".to_string()).or_insert(claude_provider);
|
||||
cfg.providers
|
||||
.entry("claude".to_string())
|
||||
.or_insert(claude_provider);
|
||||
// Register known Claude models as named model roles
|
||||
let claude_models = [
|
||||
("claude-opus-4-8", "claude-opus-4-8"),
|
||||
@@ -111,13 +122,15 @@ impl AppConfig {
|
||||
("claude-haiku-4-5", "claude-haiku-4-5-20251001"),
|
||||
];
|
||||
for (role_name, model_name) in &claude_models {
|
||||
cfg.model_roles.entry(role_name.to_string()).or_insert(ModelRole {
|
||||
provider: "claude".to_string(),
|
||||
model: model_name.to_string(),
|
||||
max_tokens: Some(8192),
|
||||
context_window: Some(200_000),
|
||||
temperature: Some(0.7),
|
||||
});
|
||||
cfg.model_roles
|
||||
.entry(role_name.to_string())
|
||||
.or_insert(ModelRole {
|
||||
provider: "claude".to_string(),
|
||||
model: model_name.to_string(),
|
||||
max_tokens: Some(8192),
|
||||
context_window: Some(200_000),
|
||||
temperature: Some(0.7),
|
||||
});
|
||||
}
|
||||
// Set as default provider only if user hasn't picked a custom default
|
||||
if cfg.default_provider == defaults.default_provider {
|
||||
@@ -154,8 +167,7 @@ struct ClaudeSettings {
|
||||
/// than through its settings file, so reading only the file misses them.
|
||||
fn detect_claude_settings_provider() -> Option<ProviderConfig> {
|
||||
// Prefer the file, then fall back to env vars.
|
||||
let (base_url, key) = claude_credentials_from_file()
|
||||
.or_else(claude_credentials_from_env)?;
|
||||
let (base_url, key) = claude_credentials_from_file().or_else(claude_credentials_from_env)?;
|
||||
Some(ProviderConfig {
|
||||
api_base: base_url,
|
||||
// Keep the env-var name so runtime env overrides still work.
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
//! In-memory conversation state: message history plus the system prompt and
|
||||
//! model parameters used to drive the LLM.
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
/// A single conversation's message history and generation settings.
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
//! Append-only JSONL edit log recording every file mutation made by tools,
|
||||
//! for audit and undo/history purposes.
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
/// A single recorded file edit: which tool made it, to which path, why,
|
||||
@@ -44,7 +43,9 @@ impl EditLog {
|
||||
/// regardless of the in-memory limit.
|
||||
fn load_from_disk(path: &std::path::Path) -> Vec<EditLogEntry> {
|
||||
use std::io::{BufRead, BufReader};
|
||||
let Ok(file) = std::fs::File::open(path) else { return Vec::new() };
|
||||
let Ok(file) = std::fs::File::open(path) else {
|
||||
return Vec::new();
|
||||
};
|
||||
let reader = BufReader::new(file);
|
||||
let mut entries: Vec<EditLogEntry> = Vec::new();
|
||||
for line in reader.lines() {
|
||||
@@ -151,7 +152,8 @@ mod tests {
|
||||
bytes_delta: 10 + i,
|
||||
origin: "main".to_string(),
|
||||
session_id: "sess-1".to_string(),
|
||||
}).unwrap();
|
||||
})
|
||||
.unwrap();
|
||||
}
|
||||
assert_eq!(log.len(), 5);
|
||||
assert_eq!(log.entries[0].reason, "reason 0");
|
||||
|
||||
+87
-26
@@ -1,8 +1,7 @@
|
||||
//! Long-term agent memory: markdown files with YAML-ish frontmatter storing
|
||||
//! lessons/references, plus slugified filenames and export/import helpers.
|
||||
|
||||
use std::path::{Path, PathBuf};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::path::{Path, PathBuf};
|
||||
|
||||
/// A single memory entry (lesson, reference, etc.) with frontmatter
|
||||
/// metadata and free-form markdown content.
|
||||
@@ -72,15 +71,30 @@ impl Memory {
|
||||
///
|
||||
/// Return: `Ok(())` on success, or an `io::Error` from directory
|
||||
/// creation, the temp write, or the rename.
|
||||
#[allow(clippy::suspicious_open_options)]
|
||||
pub fn write(&self, memory_dir: &Path) -> std::io::Result<()> {
|
||||
let path = Self::path(memory_dir, &self.name);
|
||||
let parent = path.parent().unwrap();
|
||||
std::fs::create_dir_all(parent)?;
|
||||
let outcome_line = self.outcome.as_ref().map(|o| format!("outcome: {o}")).unwrap_or_default();
|
||||
let scope_line = self.scope.as_ref().map(|s| format!("scope: {s}")).unwrap_or_default();
|
||||
let before_line = self.before_snippet.as_ref().map(|s| format!("before: {s}")).unwrap_or_default();
|
||||
let after_line = self.after_snippet.as_ref().map(|s| format!("after: {s}")).unwrap_or_default();
|
||||
let outcome_line = self
|
||||
.outcome
|
||||
.as_ref()
|
||||
.map(|o| format!("outcome: {o}"))
|
||||
.unwrap_or_default();
|
||||
let scope_line = self
|
||||
.scope
|
||||
.as_ref()
|
||||
.map(|s| format!("scope: {s}"))
|
||||
.unwrap_or_default();
|
||||
let before_line = self
|
||||
.before_snippet
|
||||
.as_ref()
|
||||
.map(|s| format!("before: {s}"))
|
||||
.unwrap_or_default();
|
||||
let after_line = self
|
||||
.after_snippet
|
||||
.as_ref()
|
||||
.map(|s| format!("after: {s}"))
|
||||
.unwrap_or_default();
|
||||
let prov_line = if self.provenances.is_empty() {
|
||||
String::new()
|
||||
} else {
|
||||
@@ -99,6 +113,7 @@ impl Memory {
|
||||
use std::io::Write;
|
||||
let mut f = std::fs::OpenOptions::new()
|
||||
.create(true)
|
||||
.truncate(true)
|
||||
.write(true)
|
||||
.open(&tmp)?;
|
||||
f.write_all(content.as_bytes())?;
|
||||
@@ -139,7 +154,10 @@ impl Memory {
|
||||
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"));
|
||||
return Err(std::io::Error::new(
|
||||
std::io::ErrorKind::InvalidData,
|
||||
"missing frontmatter",
|
||||
));
|
||||
}
|
||||
let front: std::collections::HashMap<String, String> = parts[0]
|
||||
.lines()
|
||||
@@ -153,16 +171,34 @@ impl Memory {
|
||||
name: front.get("name").cloned().unwrap_or_default(),
|
||||
description: front.get("description").cloned().unwrap_or_default(),
|
||||
content: body,
|
||||
kind: front.get("kind").cloned().unwrap_or_else(|| "reference".to_string()),
|
||||
created_at: front.get("created_at").and_then(|v| v.parse().ok()).unwrap_or(0),
|
||||
updated_at: front.get("updated_at").and_then(|v| v.parse().ok()).unwrap_or(0),
|
||||
kind: front
|
||||
.get("kind")
|
||||
.cloned()
|
||||
.unwrap_or_else(|| "reference".to_string()),
|
||||
created_at: front
|
||||
.get("created_at")
|
||||
.and_then(|v| v.parse().ok())
|
||||
.unwrap_or(0),
|
||||
updated_at: front
|
||||
.get("updated_at")
|
||||
.and_then(|v| v.parse().ok())
|
||||
.unwrap_or(0),
|
||||
outcome: front.get("outcome").cloned().filter(|s| !s.is_empty()),
|
||||
lifecycle: front.get("lifecycle").cloned().unwrap_or_else(|| "new".to_string()),
|
||||
lifecycle: front
|
||||
.get("lifecycle")
|
||||
.cloned()
|
||||
.unwrap_or_else(|| "new".to_string()),
|
||||
scope: front.get("scope").cloned().filter(|s| !s.is_empty()),
|
||||
before_snippet: front.get("before").cloned().filter(|s| !s.is_empty()),
|
||||
after_snippet: front.get("after").cloned().filter(|s| !s.is_empty()),
|
||||
provenances: front.get("provenances").cloned()
|
||||
.map(|s| s.split(", ").map(std::string::ToString::to_string).collect())
|
||||
provenances: front
|
||||
.get("provenances")
|
||||
.cloned()
|
||||
.map(|s| {
|
||||
s.split(", ")
|
||||
.map(std::string::ToString::to_string)
|
||||
.collect()
|
||||
})
|
||||
.unwrap_or_default(),
|
||||
})
|
||||
}
|
||||
@@ -186,13 +222,17 @@ impl Memory {
|
||||
/// Return: slugs (without extension); empty `Vec` if the directory
|
||||
/// can't be read.
|
||||
pub fn list(memory_dir: &Path) -> Vec<String> {
|
||||
let Ok(entries) = std::fs::read_dir(memory_dir) else { return Vec::new() };
|
||||
let Ok(entries) = std::fs::read_dir(memory_dir) else {
|
||||
return Vec::new();
|
||||
};
|
||||
entries
|
||||
.filter_map(std::result::Result::ok)
|
||||
.filter(|e| e.path().extension().is_some_and(|x| x == "md"))
|
||||
.filter_map(|e| {
|
||||
let name = e.file_name().to_string_lossy().to_string();
|
||||
if name == "MEMORY.md" { return None; }
|
||||
if name == "MEMORY.md" {
|
||||
return None;
|
||||
}
|
||||
let slug = name.strip_suffix(".md")?.to_string();
|
||||
Some(slug)
|
||||
})
|
||||
@@ -209,11 +249,22 @@ impl Memory {
|
||||
/// Why: leading-dot stripping specifically blocks accidental hidden
|
||||
/// files and `..`-style traversal attempts embedded in `raw`.
|
||||
pub fn slug_path(memory_dir: &Path, raw: &str) -> PathBuf {
|
||||
let clean: String = raw.chars()
|
||||
.map(|c| if c.is_ascii_alphanumeric() || c == '.' || c == '-' { c } else { '-' })
|
||||
let clean: String = raw
|
||||
.chars()
|
||||
.map(|c| {
|
||||
if c.is_ascii_alphanumeric() || c == '.' || c == '-' {
|
||||
c
|
||||
} else {
|
||||
'-'
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
let clean = clean.trim_start_matches('.').to_string();
|
||||
memory_dir.join(if clean.is_empty() { "memory.md" } else { &clean })
|
||||
memory_dir.join(if clean.is_empty() {
|
||||
"memory.md"
|
||||
} else {
|
||||
&clean
|
||||
})
|
||||
}
|
||||
|
||||
/// Export all memories in `memory_dir` to a single JSON file.
|
||||
@@ -227,11 +278,11 @@ pub fn slug_path(memory_dir: &Path, raw: &str) -> PathBuf {
|
||||
#[cfg(test)]
|
||||
pub fn export_lessons(memory_dir: &Path, output: &Path) -> std::io::Result<()> {
|
||||
let names = Memory::list(memory_dir);
|
||||
let lessons: Vec<Memory> = names.iter()
|
||||
let lessons: Vec<Memory> = names
|
||||
.iter()
|
||||
.filter_map(|n| Memory::read(memory_dir, n).ok())
|
||||
.collect();
|
||||
let data = serde_json::to_string_pretty(&lessons)
|
||||
.map_err(std::io::Error::other)?;
|
||||
let data = serde_json::to_string_pretty(&lessons).map_err(std::io::Error::other)?;
|
||||
// Write to temp, fsync, then rename for crash-safe export
|
||||
let tmp = output.with_extension("json.tmp");
|
||||
std::fs::write(&tmp, data)?;
|
||||
@@ -259,7 +310,8 @@ pub fn import_lessons(memory_dir: &Path, input: &Path) -> std::io::Result<usize>
|
||||
let data = std::fs::read_to_string(input)?;
|
||||
let lessons: Vec<Memory> = serde_json::from_str(&data)
|
||||
.map_err(|e| std::io::Error::new(std::io::ErrorKind::InvalidData, e))?;
|
||||
let existing: std::collections::HashSet<String> = Memory::list(memory_dir).into_iter().collect();
|
||||
let existing: std::collections::HashSet<String> =
|
||||
Memory::list(memory_dir).into_iter().collect();
|
||||
let mut imported = 0;
|
||||
for lesson in &lessons {
|
||||
let slug = Memory::slugify(&lesson.name).unwrap_or_default();
|
||||
@@ -282,12 +334,18 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn test_slugify_basic() {
|
||||
assert_eq!(Memory::slugify("Hello World"), Some("hello-world".to_string()));
|
||||
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()));
|
||||
assert_eq!(
|
||||
Memory::slugify("Use & Avoid! @#$"),
|
||||
Some("use-avoid".to_string())
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -375,7 +433,10 @@ mod tests {
|
||||
};
|
||||
mem.write(&dir).unwrap();
|
||||
let names = Memory::list(&dir);
|
||||
assert!(names.contains(&"alpha".to_string()), "list should contain 'alpha', got: {names:?}");
|
||||
assert!(
|
||||
names.contains(&"alpha".to_string()),
|
||||
"list should contain 'alpha', got: {names:?}"
|
||||
);
|
||||
let _ = std::fs::remove_dir_all(&dir);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
//! Persistence and domain model layer: sessions, conversations, memory,
|
||||
//! message log (`SQLite`), edit log, and app/settings config.
|
||||
|
||||
pub mod app_config;
|
||||
pub mod editlog;
|
||||
pub mod memory;
|
||||
|
||||
+16
-23
@@ -1,8 +1,7 @@
|
||||
//! Binary blob storage in the message-log `SQLite` database (e.g. images,
|
||||
//! attachments), keyed by session id and an arbitrary blob key.
|
||||
|
||||
use rusqlite::{Connection, params};
|
||||
use anyhow::Result;
|
||||
use rusqlite::{params, Connection};
|
||||
|
||||
/// Insert or overwrite a blob for a session under `blob_key`.
|
||||
///
|
||||
@@ -10,7 +9,13 @@ use anyhow::Result;
|
||||
/// keyed on `(session_id, blob_key)`.
|
||||
///
|
||||
/// Return: `Ok(())` on success, or the underlying `SQLite` error.
|
||||
pub fn store_blob(conn: &Connection, session_id: &str, blob_key: &str, data: &[u8], mime_type: Option<&str>) -> Result<()> {
|
||||
pub fn store_blob(
|
||||
conn: &Connection,
|
||||
session_id: &str,
|
||||
blob_key: &str,
|
||||
data: &[u8],
|
||||
mime_type: Option<&str>,
|
||||
) -> Result<()> {
|
||||
let created_at = chrono::Utc::now().timestamp_millis();
|
||||
conn.execute(
|
||||
"INSERT OR REPLACE INTO blobs (session_id, blob_key, data, mime_type, created_at) VALUES (?1, ?2, ?3, ?4, ?5)",
|
||||
@@ -23,7 +28,11 @@ pub fn store_blob(conn: &Connection, session_id: &str, blob_key: &str, data: &[u
|
||||
///
|
||||
/// Return: `Ok(Some(data))` if found, `Ok(None)` if no matching row
|
||||
/// exists, `Err` for any other `SQLite` failure.
|
||||
pub fn retrieve_blob(conn: &Connection, session_id: &str, blob_key: &str) -> Result<Option<Vec<u8>>> {
|
||||
pub fn retrieve_blob(
|
||||
conn: &Connection,
|
||||
session_id: &str,
|
||||
blob_key: &str,
|
||||
) -> Result<Option<Vec<u8>>> {
|
||||
let result = conn.query_row(
|
||||
"SELECT data FROM blobs WHERE session_id = ?1 AND blob_key = ?2",
|
||||
params![session_id, blob_key],
|
||||
@@ -36,30 +45,14 @@ pub fn retrieve_blob(conn: &Connection, session_id: &str, blob_key: &str) -> Res
|
||||
}
|
||||
}
|
||||
|
||||
/// Delete a blob for a session by key.
|
||||
///
|
||||
/// Return: `Ok(true)` if a row was deleted, `Ok(false)` if no matching
|
||||
/// row existed.
|
||||
#[allow(dead_code)]
|
||||
pub fn delete_blob(conn: &Connection, session_id: &str, blob_key: &str) -> Result<bool> {
|
||||
let rows = conn.execute(
|
||||
"DELETE FROM blobs WHERE session_id = ?1 AND blob_key = ?2",
|
||||
params![session_id, blob_key],
|
||||
)?;
|
||||
Ok(rows > 0)
|
||||
}
|
||||
|
||||
/// List all blob keys stored for a session, oldest first.
|
||||
///
|
||||
/// Return: `Ok(Vec<String>)` of keys ordered by `created_at`, or the
|
||||
/// underlying `SQLite` error.
|
||||
pub fn list_blob_keys(conn: &Connection, session_id: &str) -> Result<Vec<String>> {
|
||||
let mut stmt = conn.prepare(
|
||||
"SELECT blob_key FROM blobs WHERE session_id = ?1 ORDER BY created_at ASC"
|
||||
)?;
|
||||
let rows = stmt.query_map(params![session_id], |row| {
|
||||
row.get::<_, String>(0)
|
||||
})?;
|
||||
let mut stmt =
|
||||
conn.prepare("SELECT blob_key FROM blobs WHERE session_id = ?1 ORDER BY created_at ASC")?;
|
||||
let rows = stmt.query_map(params![session_id], |row| row.get::<_, String>(0))?;
|
||||
let mut keys = Vec::new();
|
||||
for row in rows {
|
||||
keys.push(row?);
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
//! SQLite-backed message log: per-session `messages.sqlite` storing chat
|
||||
//! messages, blobs, and archive/summary metadata.
|
||||
|
||||
pub mod blobs;
|
||||
pub mod query;
|
||||
pub mod schema;
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
//! Insert queries against the message log's `messages` table.
|
||||
|
||||
use rusqlite::{Connection, params};
|
||||
use anyhow::Result;
|
||||
use crate::dto::chat::message::{ChatMessage, Role};
|
||||
use anyhow::Result;
|
||||
use rusqlite::{params, Connection};
|
||||
|
||||
/// Insert a chat message into the session's message log.
|
||||
///
|
||||
@@ -15,9 +14,10 @@ pub fn insert_message(conn: &Connection, session_id: &str, msg: &ChatMessage) ->
|
||||
let content = msg.content.as_deref();
|
||||
let tool_call_id = msg.tool_call_id.as_deref();
|
||||
let tool_name = msg.name.as_deref();
|
||||
let tool_arguments = msg.tool_calls.as_ref().map(|calls| {
|
||||
serde_json::to_string(calls).unwrap_or_default()
|
||||
});
|
||||
let tool_arguments = msg
|
||||
.tool_calls
|
||||
.as_ref()
|
||||
.map(|calls| serde_json::to_string(calls).unwrap_or_default());
|
||||
let created_at = chrono::Utc::now().timestamp_millis();
|
||||
let role_str = match msg.role {
|
||||
Role::User => "user",
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
//! `SQLite` schema definition for the message log database.
|
||||
|
||||
use rusqlite::Connection;
|
||||
use anyhow::Result;
|
||||
use rusqlite::Connection;
|
||||
|
||||
/// Create the message log's tables and indexes if they don't already
|
||||
/// exist (`messages`, `archives`, `blobs`).
|
||||
@@ -51,7 +50,7 @@ pub fn init_schema(conn: &Connection) -> Result<()> {
|
||||
created_at INTEGER NOT NULL,
|
||||
UNIQUE(session_id, blob_key)
|
||||
);
|
||||
"
|
||||
",
|
||||
)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
//! Session archive/summary metadata tracked alongside the message log
|
||||
//! (title, model, counts, and a rolling text summary).
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
/// Summary metadata for one archived/summarized session.
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
//! Session metadata: id, title, workspace roots, and message/token counts,
|
||||
//! persisted as `session.json` per session directory.
|
||||
|
||||
use std::path::{Path, PathBuf};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use chrono::Utc;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::path::{Path, PathBuf};
|
||||
|
||||
/// Metadata for one conversation session (distinct from the message
|
||||
/// history itself, which lives in `Conversation`/the msglog).
|
||||
@@ -108,7 +107,9 @@ impl Session {
|
||||
/// contains no valid sessions.
|
||||
pub fn list(base_dir: &Path) -> Vec<Self> {
|
||||
let sessions_dir = base_dir.join("sessions");
|
||||
let Ok(entries) = std::fs::read_dir(&sessions_dir) else { return Vec::new() };
|
||||
let Ok(entries) = std::fs::read_dir(&sessions_dir) else {
|
||||
return Vec::new();
|
||||
};
|
||||
entries
|
||||
.filter_map(std::result::Result::ok)
|
||||
.filter(|e| e.path().is_dir())
|
||||
|
||||
+18
-11
@@ -1,10 +1,14 @@
|
||||
#![allow(clippy::cast_possible_truncation, clippy::cast_sign_loss, clippy::cast_precision_loss, clippy::cast_possible_wrap)]
|
||||
#![allow(
|
||||
clippy::cast_possible_truncation,
|
||||
clippy::cast_sign_loss,
|
||||
clippy::cast_precision_loss,
|
||||
clippy::cast_possible_wrap
|
||||
)]
|
||||
//! PID-file based advisory lock preventing two processes from operating on
|
||||
//! the same session directory concurrently.
|
||||
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::fs;
|
||||
use std::io::Write;
|
||||
use std::path::{Path, PathBuf};
|
||||
|
||||
/// A PID-file lock (`<session_dir>/.lock`) tied to the current process,
|
||||
/// auto-removed on drop.
|
||||
@@ -38,7 +42,6 @@ impl SessionLock {
|
||||
///
|
||||
/// Return: `Ok(true)` if acquired, `Ok(false)` if another live
|
||||
/// process holds it, `Err` on I/O failure.
|
||||
#[allow(clippy::suspicious_open_options)]
|
||||
pub fn try_lock(&self) -> std::io::Result<bool> {
|
||||
// Phase 1: try atomic create. If it succeeds, the lock is ours.
|
||||
match fs::OpenOptions::new()
|
||||
@@ -60,7 +63,7 @@ impl SessionLock {
|
||||
// Phase 2: lock file exists — check liveness of the owning process.
|
||||
let content = fs::read_to_string(&self.path).unwrap_or_default();
|
||||
if let Ok(pid) = content.trim().parse::<u32>() {
|
||||
if self.is_alive(pid) {
|
||||
if Self::is_alive(pid) {
|
||||
return Ok(false);
|
||||
}
|
||||
}
|
||||
@@ -71,6 +74,7 @@ impl SessionLock {
|
||||
{
|
||||
let mut tmp_file = fs::OpenOptions::new()
|
||||
.create(true)
|
||||
.truncate(true)
|
||||
.write(true)
|
||||
.open(&tmp)?;
|
||||
write!(tmp_file, "{}", self.pid)?;
|
||||
@@ -92,8 +96,7 @@ impl SessionLock {
|
||||
/// Check whether a process with the given PID is currently alive and
|
||||
/// is actually a zesdex process (not a recycled PID from a different
|
||||
/// program).
|
||||
#[allow(clippy::unused_self)]
|
||||
fn is_alive(&self, pid: u32) -> bool {
|
||||
fn is_alive(pid: u32) -> bool {
|
||||
// SAFETY: `libc::kill(pid, 0)` does not send a signal; it only checks
|
||||
// whether the process exists and the caller has permission to signal
|
||||
// it. The integer argument is a PID already validated by `try_lock`.
|
||||
@@ -106,11 +109,15 @@ impl SessionLock {
|
||||
// our lock). This is best-effort — /proc may not be available
|
||||
// on all platforms.
|
||||
let proc_exe = std::path::PathBuf::from(format!("/proc/{pid}/exe"));
|
||||
if let Ok(target) = std::fs::read_link(&proc_exe) { if let Ok(exe) = std::env::current_exe() {
|
||||
if target != exe {
|
||||
return false;
|
||||
if let Ok(target) = std::fs::read_link(&proc_exe) {
|
||||
if let Ok(exe) = std::env::current_exe() {
|
||||
if target != exe {
|
||||
return false;
|
||||
}
|
||||
} else { /* cannot resolve own exe, trust kill check */
|
||||
}
|
||||
} else { /* cannot resolve own exe, trust kill check */ } } else { /* /proc unavailable, trust kill check */ }
|
||||
} else { /* /proc unavailable, trust kill check */
|
||||
}
|
||||
true
|
||||
}
|
||||
}
|
||||
|
||||
+25
-49
@@ -24,6 +24,24 @@ fn default_hive_mind_node_timeout_ms() -> u64 {
|
||||
600_000
|
||||
}
|
||||
|
||||
/// Boolean flags grouped to keep the top-level struct below clippy's bool threshold.
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct SettingsFlags {
|
||||
pub review_enabled: bool,
|
||||
pub session_archive_enabled: bool,
|
||||
pub lsp_auto_provision: bool,
|
||||
}
|
||||
|
||||
impl Default for SettingsFlags {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
review_enabled: true,
|
||||
session_archive_enabled: true,
|
||||
lsp_auto_provision: true,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Top-level application settings, serialized to `settings.json` in the store dir.
|
||||
///
|
||||
/// Why: a single flat struct rather than nested config so the JSON file stays
|
||||
@@ -36,28 +54,21 @@ pub struct Settings {
|
||||
pub api_keys: std::collections::HashMap<String, String>,
|
||||
pub max_tokens: Option<u32>,
|
||||
pub temperature: Option<f32>,
|
||||
pub review_enabled: bool,
|
||||
pub review_max_lessons_per_run: usize,
|
||||
pub adaptive_review_max_skip: u32,
|
||||
pub verify_command: Option<String>,
|
||||
pub verify_timeout_ms: u64,
|
||||
pub workflow_max_concurrency: usize,
|
||||
pub session_archive_enabled: bool,
|
||||
pub lsp_auto_provision: bool,
|
||||
/// Boolean flags flattened into the top-level JSON so existing settings
|
||||
/// files remain compatible when bools are grouped into a sub-struct.
|
||||
#[serde(flatten)]
|
||||
pub flags: SettingsFlags,
|
||||
pub lsp_languages: Vec<String>,
|
||||
/// Wall-clock deadline for a single hive-mind processing node (cycle
|
||||
/// node or synthesis node). Prevents one stuck node from hanging an
|
||||
/// entire hive-mind convergence forever.
|
||||
#[serde(default = "default_hive_mind_node_timeout_ms")]
|
||||
pub hive_mind_node_timeout_ms: u64,
|
||||
/// Off by default. When enabled, appends an instruction to the
|
||||
/// system prompt asking the model to write tersely — drop articles,
|
||||
/// filler words, hedging, and pleasantries; keep code, commands, and
|
||||
/// error text byte-exact — with an explicit exception for
|
||||
/// destructive-operation confirmations and security warnings, which
|
||||
/// always get full detail regardless of this setting.
|
||||
#[serde(default)]
|
||||
pub concise_output: bool,
|
||||
}
|
||||
|
||||
impl Default for Settings {
|
||||
@@ -69,17 +80,14 @@ impl Default for Settings {
|
||||
api_keys: std::collections::HashMap::new(),
|
||||
max_tokens: None,
|
||||
temperature: None,
|
||||
review_enabled: true,
|
||||
review_max_lessons_per_run: 5,
|
||||
adaptive_review_max_skip: 3,
|
||||
verify_command: None,
|
||||
verify_timeout_ms: 30000,
|
||||
workflow_max_concurrency: 5,
|
||||
session_archive_enabled: true,
|
||||
lsp_auto_provision: true,
|
||||
flags: SettingsFlags::default(),
|
||||
lsp_languages: Vec::new(),
|
||||
hive_mind_node_timeout_ms: default_hive_mind_node_timeout_ms(),
|
||||
concise_output: false,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -132,38 +140,6 @@ mod tests {
|
||||
assert_eq!(settings.hive_mind_node_timeout_ms, 600_000);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn concise_output_defaults_to_false() {
|
||||
assert!(!Settings::default().concise_output);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn missing_concise_output_field_falls_back_to_default() {
|
||||
// Simulates loading a settings.json written before this field
|
||||
// existed — #[serde(default)] must fill it in rather than
|
||||
// failing the whole parse.
|
||||
let old_json = r#"{
|
||||
"internet_mode": "Off",
|
||||
"provider": "zen",
|
||||
"model": "deepseek-v4-flash-free",
|
||||
"api_keys": {},
|
||||
"max_tokens": null,
|
||||
"temperature": null,
|
||||
"review_enabled": true,
|
||||
"review_max_lessons_per_run": 5,
|
||||
"adaptive_review_max_skip": 3,
|
||||
"verify_command": null,
|
||||
"verify_timeout_ms": 30000,
|
||||
"workflow_max_concurrency": 5,
|
||||
"session_archive_enabled": true,
|
||||
"lsp_auto_provision": true,
|
||||
"lsp_languages": []
|
||||
}"#;
|
||||
let parsed: Settings = serde_json::from_str(old_json)
|
||||
.expect("must parse even without the new field present");
|
||||
assert!(!parsed.concise_output);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn missing_hive_mind_node_timeout_field_falls_back_to_default() {
|
||||
// Simulates loading a settings.json written before this field
|
||||
@@ -178,13 +154,13 @@ mod tests {
|
||||
"max_tokens": null,
|
||||
"temperature": null,
|
||||
"review_enabled": true,
|
||||
"session_archive_enabled": true,
|
||||
"lsp_auto_provision": true,
|
||||
"review_max_lessons_per_run": 5,
|
||||
"adaptive_review_max_skip": 3,
|
||||
"verify_command": null,
|
||||
"verify_timeout_ms": 30000,
|
||||
"workflow_max_concurrency": 5,
|
||||
"session_archive_enabled": true,
|
||||
"lsp_auto_provision": true,
|
||||
"lsp_languages": []
|
||||
}"#;
|
||||
let parsed: Settings = serde_json::from_str(old_json)
|
||||
|
||||
+1
-2
@@ -1,7 +1,6 @@
|
||||
//! Filesystem layout for zesdex's persistent and scratch data directories.
|
||||
|
||||
use std::path::PathBuf;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::path::PathBuf;
|
||||
|
||||
/// Resolved paths for all data directories zesdex reads from and writes to.
|
||||
///
|
||||
|
||||
Reference in New Issue
Block a user