refactor: DRY cleanup — extract shared helpers, remove duplication across tools, LSP, overlays, and runtime

Eliminate ~500 lines of duplicate code across 31 files by extracting
shared functions, helpers, and consolidating repeated patterns.

Highlights:
- Toast helpers (toast_info/success/warning/error) on AppStateRest
- push_event() helper for turn-event queue (19 callers consolidated)
- log_write_edit_tool() shared fn (turn.rs + engine.rs ~50 lines saved)
- resolve_api_key() shared fn (spawn.rs + provider.rs)
- LSP call_positional() helper on LspClient
- lsp_cursor_params() shared schema for 4 tool files
-overlay_block() helper for consistent overlay title/border styling
- cycle_selected_index(), path_not_found/a_directory() helpers
- mark_dirty(), save_settings() on AppStateRest
- Remove redundant Err(e) => Err(e) arms in LSP tools
- Consolidate generate_workspace_tree (turn.rs → workspace.rs)
- Simplify background-review wrapper args in auto/mod.rs

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
asepharyana
2026-07-18 03:18:27 +07:00
co-authored by Claude Opus 4.8
parent b02754acd2
commit 9a6ab62562
31 changed files with 628 additions and 870 deletions
@@ -248,16 +248,24 @@ fn spawn_background_review(
});
}
/// Collect the trailing arguments shared by all background-review spawners.
fn review_args<'a>(
file_paths: &'a [String],
session_dir: &'a Path,
workspaces: &'a [std::path::PathBuf],
turn_events: &'a Arc<Mutex<VecDeque<TurnEvent>>>,
abort_flag: Arc<AtomicBool>,
) -> (Vec<String>, std::path::PathBuf, Vec<std::path::PathBuf>, Arc<Mutex<VecDeque<TurnEvent>>>, Arc<AtomicBool>) {
(
file_paths.to_vec(),
session_dir.to_path_buf(),
workspaces.to_vec(),
turn_events.clone(),
abort_flag,
)
}
/// Spawn a background subagent that generates tests for modified files.
///
/// Uses the test-generator prompt and has read-write access so it can
/// create test files. Runs in a separate OS thread and reports completion
/// via `TurnEvent::SystemNote { kind: "bg-test-gen" }`.
///
/// Skipped (no-op) if a test-gen run is already in flight (guarded by
/// `TEST_GEN_RUNNING`) — prevents a chatty multi-turn edit session from
/// stacking overlapping runs. `abort_flag` is forwarded to the generic
/// spawner so the run can be cancelled if the turn aborts.
pub fn spawn_background_test_gen(
file_paths: &[String],
session_dir: &Path,
@@ -265,29 +273,15 @@ pub fn spawn_background_test_gen(
turn_events: &Arc<Mutex<VecDeque<TurnEvent>>>,
abort_flag: Arc<AtomicBool>,
) {
let (fps, sd, ws, te, af) = review_args(file_paths, session_dir, workspaces, turn_events, abort_flag);
spawn_background_review(
"bg-test-gen",
&TEST_GEN_RUNNING,
crate::prompts::TEST_GENERATOR_PROMPT,
"test-generator",
"coder",
file_paths.to_vec(),
session_dir.to_path_buf(),
workspaces.to_vec(),
turn_events.clone(),
abort_flag,
"bg-test-gen", &TEST_GEN_RUNNING,
crate::prompts::TEST_GENERATOR_PROMPT, "test-generator", "coder",
fps, sd, ws, te, af,
);
}
/// Spawn a background architecture-review subagent.
///
/// Inspects the modified files for architectural consistency (layering,
/// coupling, module boundaries). Reports via
/// `TurnEvent::SystemNote { kind: "bg-arch-review" }`.
///
/// Skipped (no-op) if an arch-review run is already in flight (guarded by
/// `ARCH_REVIEW_RUNNING`). `abort_flag` is forwarded to the generic
/// spawner so the run can be cancelled if the turn aborts.
pub fn spawn_background_arch_review(
file_paths: &[String],
session_dir: &Path,
@@ -295,31 +289,18 @@ pub fn spawn_background_arch_review(
turn_events: &Arc<Mutex<VecDeque<TurnEvent>>>,
abort_flag: Arc<AtomicBool>,
) {
let (fps, sd, ws, te, af) = review_args(file_paths, session_dir, workspaces, turn_events, abort_flag);
spawn_background_review(
"bg-arch-review",
&ARCH_REVIEW_RUNNING,
crate::prompts::ARCH_REVIEWER_PROMPT,
"arch-reviewer",
"reviewer",
file_paths.to_vec(),
session_dir.to_path_buf(),
workspaces.to_vec(),
turn_events.clone(),
abort_flag,
"bg-arch-review", &ARCH_REVIEW_RUNNING,
crate::prompts::ARCH_REVIEWER_PROMPT, "arch-reviewer", "reviewer",
fps, sd, ws, te, af,
);
}
/// Spawn a background security-review subagent.
///
/// Checks modified files for security vulnerabilities. Reports via
/// `TurnEvent::SystemNote { kind: "bg-security-review" }`.
///
/// Only reviews production code files for security — test files and
/// config files are out of scope for security review.
///
/// Skipped (no-op) if a security-review run is already in flight (guarded by
/// `SECURITY_REVIEW_RUNNING`). `abort_flag` is forwarded to the generic
/// spawner so the run can be cancelled if the turn aborts.
pub fn spawn_background_security_review(
file_paths: &[String],
session_dir: &Path,
@@ -327,25 +308,16 @@ pub fn spawn_background_security_review(
turn_events: &Arc<Mutex<VecDeque<TurnEvent>>>,
abort_flag: Arc<AtomicBool>,
) {
// Only review production code files for security — test files and
// config files are out of scope for security review.
let (_, sd, ws, te, af) = review_args(file_paths, session_dir, workspaces, turn_events, abort_flag);
let prod_paths: Vec<String> = file_paths
.iter()
.filter(|p| is_production_code(p))
.cloned()
.collect();
spawn_background_review(
"bg-security-review",
&SECURITY_REVIEW_RUNNING,
crate::prompts::SECURITY_REVIEWER_PROMPT,
"security-reviewer",
"reviewer",
prod_paths,
session_dir.to_path_buf(),
workspaces.to_vec(),
turn_events.clone(),
abort_flag,
"bg-security-review", &SECURITY_REVIEW_RUNNING,
crate::prompts::SECURITY_REVIEWER_PROMPT, "security-reviewer", "reviewer",
prod_paths, sd, ws, te, af,
);
}
@@ -14,10 +14,8 @@ use super::workspace::generate_workspace_tree;
use crate::dto::chat::message::ChatMessage;
use crate::dto::provider::request::ToolDef;
use crate::tool::tool_is_risky;
use sha2::Digest;
use std::time::{Duration, SystemTime, UNIX_EPOCH};
use tokio::sync::mpsc;
use zesdex_cms::domain::repository::EditLogRepository;
/// Tiny jitter helper so retry backoffs don't arrive in lockstep.
fn retry_jitter_ns(range_ns: u64) -> u64 {
@@ -208,6 +206,7 @@ pub fn run_subagent(
}
true
},
ctx.abort_flag.as_deref(),
);
match stream_result {
@@ -346,49 +345,14 @@ pub fn run_subagent(
let run_res = tool.run(tool_ctx_ref, &args);
if is_edit && run_res.is_ok() {
let reason = args
.get("reason")
.and_then(|v| v.as_str())
.unwrap_or("unnamed");
let path = args
.get("path")
.and_then(|v| v.as_str())
.unwrap_or("unknown");
let content_sha256 = {
let content = args.get("content").or_else(|| args.get("new"));
let hash = sha2::Sha256::digest(
content.and_then(|v| v.as_str()).unwrap_or("").as_bytes(),
);
hex::encode(hash)
};
let bytes_delta = if tool_name == "write" {
args.get("content")
.and_then(|v| v.as_str())
.map_or(0, |s| s.len() as i64)
} else {
let old = args.get("old").and_then(|v| v.as_str()).unwrap_or("");
let new = args.get("new").and_then(|v| v.as_str()).unwrap_or("");
(new.len() as i64 - old.len() as i64).abs()
};
let session_id = ctx.session_dir
.file_name()
.and_then(|n| n.to_str())
.unwrap_or("unknown")
.to_string();
let entry = zesdex_cms::domain::edit_log::EditLogEntry {
ts: chrono::Utc::now().timestamp_millis(),
tool: tool_name.clone(),
path: path.to_string(),
reason: reason.to_string(),
content_sha256,
bytes_delta,
origin: tool_ctx_ref.origin.tag(),
session_id,
};
let repo = zesdex_cms::infrastructure::persistence::edit_log_repo::JsonlEditLogRepository::new();
if let Ok(mut el) = repo.open(&ctx.session_dir) {
let _ = repo.append(&ctx.session_dir, &mut el, entry);
}
.unwrap_or("unknown");
crate::tool::log_write_edit_tool(
&args, tool_name, &tool_ctx_ref.origin.tag(),
&ctx.session_dir, session_id,
);
}
run_res
}
@@ -27,40 +27,19 @@ pub(crate) fn resolve_provider_config() -> (String, String, Option<String>, Stri
.load(&store_base_dir)
.unwrap_or_default();
let mut api_key = settings
.api_keys
.get(&settings.provider)
.cloned()
.unwrap_or_else(|| {
tracing::warn!(
"[subagent] no API key for provider '{}' in settings, trying env/default",
settings.provider
);
String::new()
});
let api_key = crate::service::provider::resolve_api_key(&settings, &app_config);
if api_key.is_empty() {
tracing::warn!(
"[subagent] all API key resolution paths exhausted for '{}'",
settings.provider
);
}
let model = settings.model.clone();
let base_url = app_config
.providers
.get(&settings.provider)
.map(|p| p.api_base.clone());
if api_key.is_empty() {
if let Some(provider_cfg) = app_config.providers.get(&settings.provider) {
api_key = provider_cfg
.api_key_env
.as_ref()
.and_then(|env| std::env::var(env).ok())
.or_else(|| provider_cfg.default_api_key.clone())
.unwrap_or_else(|| {
tracing::warn!(
"[subagent] all API key resolution paths exhausted for '{}'",
settings.provider
);
String::new()
});
}
}
(api_key, model, base_url, settings.provider)
}