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:
asepharyana
2026-07-16 07:56:11 +07:00
parent 7d99cd6618
commit a00aa9bec8
141 changed files with 3420 additions and 2172 deletions
-1
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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.
+5 -3
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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;
+6 -6
View File
@@ -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",
+2 -3
View File
@@ -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
View File
@@ -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.
+5 -4
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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.
///