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:
co-authored by
Claude Opus 4.8
parent
b02754acd2
commit
9a6ab62562
@@ -57,12 +57,9 @@ pub(super) fn spawn_turn(state: &AppStateRest) {
|
||||
if messages.is_empty() {
|
||||
return;
|
||||
}
|
||||
let mut api_key = state
|
||||
.settings
|
||||
.api_keys
|
||||
.get(&state.settings.provider)
|
||||
.cloned()
|
||||
.unwrap_or_default();
|
||||
let mut api_key = crate::service::provider::resolve_api_key(
|
||||
&state.settings, &state.app_config,
|
||||
);
|
||||
let model = state.settings.model.clone();
|
||||
let base_url = state
|
||||
.app_config
|
||||
@@ -95,16 +92,6 @@ pub(super) fn spawn_turn(state: &AppStateRest) {
|
||||
}
|
||||
return;
|
||||
}
|
||||
if api_key.is_empty() {
|
||||
if let Some(provider_cfg) = state.app_config.providers.get(&state.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_default();
|
||||
}
|
||||
}
|
||||
if api_key.is_empty() {
|
||||
api_key = crate::service::provider::DEFAULT_API_KEY.to_string();
|
||||
}
|
||||
|
||||
@@ -271,6 +271,19 @@ pub(super) fn handle_tick(state: &mut AppStateRest) {
|
||||
}
|
||||
TurnEvent::StreamDone(msg) => {
|
||||
state.misc.thinking = false;
|
||||
// Replace the partial streaming transcript with the complete
|
||||
// message content. In the normal streaming path this is a
|
||||
// no-op (the accumulated tokens already match), but when the
|
||||
// non-streaming fallback fires the response is a completely
|
||||
// new generation — the partial SSE text must be overwritten.
|
||||
if let Some(content) = &msg.content {
|
||||
if let Some(last) = state.transcript_cache.messages.last_mut() {
|
||||
if last.role == Role::Assistant {
|
||||
last.content.clone_from(content);
|
||||
state.transcript_cache.dirty = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
if let Some(ref mut rt) = state.session_runtime {
|
||||
rt.push_message(msg);
|
||||
}
|
||||
|
||||
@@ -3,18 +3,17 @@
|
||||
//! messages, and manages auto-retry for unfinished tasks.
|
||||
//!
|
||||
//! Also contains the smaller helpers that the loop depends on:
|
||||
//! `execute_one_tool`, `generate_workspace_tree`, `build_memory_section`,
|
||||
//! and `archive_message`.
|
||||
//! `execute_one_tool`, `build_memory_section`, and `archive_message`.
|
||||
|
||||
use std::collections::VecDeque;
|
||||
use std::fmt::Write;
|
||||
|
||||
use sha2::Digest;
|
||||
use zesdex_cms::domain::repository::EditLogRepository;
|
||||
|
||||
use crate::app::guard::Verdict;
|
||||
use crate::app::runtime::context::tokens::count_tokens;
|
||||
use crate::app::runtime::push_event;
|
||||
use crate::app::state::runtime::TurnEvent;
|
||||
use zesdex_cms::domain::repository::EditLogRepository;
|
||||
use zesdex_cms::domain::repository::MemoryRepository;
|
||||
use crate::dto::chat::message::ChatMessage;
|
||||
|
||||
@@ -66,17 +65,15 @@ pub(super) fn run_agent_turn(
|
||||
const MAX_TODO_RETRIES: usize = 5;
|
||||
let mut msgs = messages.to_vec();
|
||||
let mut edited_paths: Vec<String> = Vec::new();
|
||||
let initial_edits = zesdex_cms::infrastructure::persistence::edit_log_repo::JsonlEditLogRepository::new()
|
||||
.open(&tc.edit_log_session_dir)
|
||||
.map(|el| el.len())
|
||||
.unwrap_or(0);
|
||||
let initial_edit_log = zesdex_cms::infrastructure::persistence::edit_log_repo::JsonlEditLogRepository::new()
|
||||
.open(&tc.edit_log_session_dir).ok();
|
||||
let mut inline_reviews_count: usize = 0;
|
||||
let mut prev_shaped = false;
|
||||
|
||||
// Build system prompt components once and cache them for the entire turn
|
||||
// instead of regenerating on every loop iteration (which walks the full
|
||||
// workspace tree and reads all memory files each time).
|
||||
let tree_info = generate_workspace_tree(&tc.workspace_roots);
|
||||
let tree_info = crate::app::subagent::workspace::generate_workspace_tree(&tc.workspace_roots);
|
||||
let memory_section = build_memory_section(&tc.ctx.memory_dir);
|
||||
let system_text = format!(
|
||||
"{}\n\n{}\n\n{}{}",
|
||||
@@ -142,12 +139,10 @@ pub(super) fn run_agent_turn(
|
||||
"[hive-mind] the Hive stirs — Core Intelligence compiling a cognitive cycle plan"
|
||||
);
|
||||
|
||||
if let Ok(mut q) = events_q.lock() {
|
||||
q.push_back(TurnEvent::SystemNote {
|
||||
kind: "pipeline".to_string(),
|
||||
message: HIVE_MIND_KICKOFF_NOTE.to_string(),
|
||||
});
|
||||
}
|
||||
push_event(&events_q, TurnEvent::SystemNote {
|
||||
kind: "pipeline".to_string(),
|
||||
message: HIVE_MIND_KICKOFF_NOTE.to_string(),
|
||||
});
|
||||
|
||||
let pipeline_abort = Some(tc.abort_flag.clone());
|
||||
|
||||
@@ -194,8 +189,13 @@ pub(super) fn run_agent_turn(
|
||||
|
||||
let planner_prompt_chars = system_msg.content.as_deref().map_or(0, str::len)
|
||||
+ user_msg.content.as_deref().map_or(0, str::len);
|
||||
let planner_result =
|
||||
tc.client.chat_with_tools_non_streaming(&[system_msg, user_msg], None);
|
||||
let planner_result = tc.client.chat_with_tools_non_streaming(
|
||||
&[system_msg, user_msg],
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
Some(&tc.abort_flag),
|
||||
);
|
||||
let pipeline_result = match planner_result {
|
||||
Ok((reply, usage_opt)) => {
|
||||
let (mut tok_in, mut tok_out) = usage_opt.unwrap_or((0, 0));
|
||||
@@ -206,12 +206,10 @@ pub(super) fn run_agent_turn(
|
||||
let response_chars = reply.content.as_deref().map_or(0, str::len);
|
||||
tok_out = (response_chars / 4).max(1) as u64;
|
||||
}
|
||||
if let Ok(mut q) = events_q.lock() {
|
||||
q.push_back(TurnEvent::Usage {
|
||||
tokens_in: tok_in,
|
||||
tokens_out: tok_out,
|
||||
});
|
||||
}
|
||||
push_event(&events_q, TurnEvent::Usage {
|
||||
tokens_in: tok_in,
|
||||
tokens_out: tok_out,
|
||||
});
|
||||
let reply_text = reply.content.as_deref().unwrap_or("").trim();
|
||||
let clean_json = if reply_text.starts_with("```") {
|
||||
let mut lines = reply_text.lines();
|
||||
@@ -238,15 +236,13 @@ pub(super) fn run_agent_turn(
|
||||
.collect::<Vec<String>>()
|
||||
.join(", ");
|
||||
|
||||
if let Ok(mut q) = events_q.lock() {
|
||||
q.push_back(TurnEvent::SystemNote {
|
||||
kind: "pipeline".to_string(),
|
||||
message: format!(
|
||||
"The Hive compiled {} cycle(s) — {cycle_desc}. Deploying nodes...",
|
||||
plan.cycles.len()
|
||||
),
|
||||
});
|
||||
}
|
||||
push_event(&events_q, TurnEvent::SystemNote {
|
||||
kind: "pipeline".to_string(),
|
||||
message: format!(
|
||||
"The Hive compiled {} cycle(s) — {cycle_desc}. Deploying nodes...",
|
||||
plan.cycles.len()
|
||||
),
|
||||
});
|
||||
|
||||
crate::app::workflow::hive_mind::run_hive_mind(
|
||||
user_request,
|
||||
@@ -283,20 +279,16 @@ pub(super) fn run_agent_turn(
|
||||
archive_message(tc.db.as_ref(), &tc.session_id, &pipeline_msg);
|
||||
msgs.push(pipeline_msg);
|
||||
|
||||
if let Ok(mut q) = events_q.lock() {
|
||||
q.push_back(TurnEvent::SystemNote {
|
||||
kind: "pipeline".to_string(),
|
||||
message:
|
||||
"The Hive's convergence is complete. Core Intelligence reviewing consensus for LO..."
|
||||
.to_string(),
|
||||
});
|
||||
}
|
||||
if let Ok(mut q) = events_q.lock() {
|
||||
q.push_back(TurnEvent::SystemNote {
|
||||
kind: "hive_mind_converged".to_string(),
|
||||
message: String::new(),
|
||||
});
|
||||
}
|
||||
push_event(&events_q, TurnEvent::SystemNote {
|
||||
kind: "pipeline".to_string(),
|
||||
message:
|
||||
"The Hive's convergence is complete. Core Intelligence reviewing consensus for LO..."
|
||||
.to_string(),
|
||||
});
|
||||
push_event(&events_q, TurnEvent::SystemNote {
|
||||
kind: "hive_mind_converged".to_string(),
|
||||
message: String::new(),
|
||||
});
|
||||
}
|
||||
Err(e) => {
|
||||
tracing::warn!("[hive-mind] convergence fractured: {}", e);
|
||||
@@ -318,9 +310,7 @@ pub(super) fn run_agent_turn(
|
||||
.abort_flag
|
||||
.load(std::sync::atomic::Ordering::SeqCst)
|
||||
{
|
||||
if let Ok(mut q) = events_q.lock() {
|
||||
q.push_back(TurnEvent::Error("Generation aborted by user".to_string()));
|
||||
}
|
||||
push_event(&events_q, TurnEvent::Error("Generation aborted by user".to_string()));
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
@@ -357,9 +347,7 @@ pub(super) fn run_agent_turn(
|
||||
|
||||
// Dispatch the compacted messages to the main thread so the local session history
|
||||
// is permanently compacted and doesn't trigger shaping again immediately on next turn.
|
||||
if let Ok(mut q) = events_q.lock() {
|
||||
q.push_back(TurnEvent::Compacted(compacted.clone()));
|
||||
}
|
||||
push_event(&events_q, TurnEvent::Compacted(compacted.clone()));
|
||||
|
||||
// Also update our local `msgs` variable so the rest of the loop operates on the compacted version
|
||||
msgs.clone_from(&compacted);
|
||||
@@ -424,14 +412,13 @@ pub(super) fn run_agent_turn(
|
||||
}
|
||||
true
|
||||
},
|
||||
Some(&tc.abort_flag),
|
||||
);
|
||||
|
||||
if reasoning_started && !reasoning_ended {
|
||||
if let Ok(mut q) = events_q.lock() {
|
||||
q.push_back(TurnEvent::StreamToken(
|
||||
"\n</think>\n\n".to_string(),
|
||||
));
|
||||
}
|
||||
push_event(&events_q, TurnEvent::StreamToken(
|
||||
"\n</think>\n\n".to_string(),
|
||||
));
|
||||
}
|
||||
|
||||
let (response, final_usage) = match result {
|
||||
@@ -441,11 +428,9 @@ pub(super) fn run_agent_turn(
|
||||
if tc.abort_flag.load(std::sync::atomic::Ordering::SeqCst)
|
||||
|| e.to_string().contains("aborted")
|
||||
{
|
||||
if let Ok(mut q) = events_q.lock() {
|
||||
q.push_back(TurnEvent::Error(
|
||||
"Generation aborted by user".to_string(),
|
||||
));
|
||||
}
|
||||
push_event(&events_q, TurnEvent::Error(
|
||||
"Generation aborted by user".to_string(),
|
||||
));
|
||||
return Ok(());
|
||||
}
|
||||
// Streaming-only: no non-streaming fallback.
|
||||
@@ -472,14 +457,12 @@ pub(super) fn run_agent_turn(
|
||||
Edit todo.md manually or ask me to focus on specific items.",
|
||||
);
|
||||
}
|
||||
if let Ok(mut q) = events_q.lock() {
|
||||
q.push_back(TurnEvent::SystemNote {
|
||||
kind: "task_retry".to_string(),
|
||||
message: format!(
|
||||
"Network/API error: {api_err}. Auto-retrying to finish tasks... (retry {todo_retry_count}/{MAX_TODO_RETRIES})"
|
||||
),
|
||||
});
|
||||
}
|
||||
push_event(&events_q, TurnEvent::SystemNote {
|
||||
kind: "task_retry".to_string(),
|
||||
message: format!(
|
||||
"Network/API error: {api_err}. Auto-retrying to finish tasks... (retry {todo_retry_count}/{MAX_TODO_RETRIES})"
|
||||
),
|
||||
});
|
||||
std::thread::sleep(std::time::Duration::from_secs(5));
|
||||
continue;
|
||||
}
|
||||
@@ -500,12 +483,10 @@ pub(super) fn run_agent_turn(
|
||||
let response_chars = response.content.as_deref().map_or(0, str::len);
|
||||
tok_out = (response_chars / 4).max(1) as u64;
|
||||
}
|
||||
if let Ok(mut q) = events_q.lock() {
|
||||
q.push_back(TurnEvent::Usage {
|
||||
tokens_in: tok_in,
|
||||
tokens_out: tok_out,
|
||||
});
|
||||
}
|
||||
push_event(&events_q, TurnEvent::Usage {
|
||||
tokens_in: tok_in,
|
||||
tokens_out: tok_out,
|
||||
});
|
||||
|
||||
let has_tool_calls = response.tool_calls.is_some()
|
||||
&& response.tool_calls.as_ref().is_some_and(|tc| !tc.is_empty());
|
||||
@@ -575,11 +556,9 @@ pub(super) fn run_agent_turn(
|
||||
.abort_flag
|
||||
.load(std::sync::atomic::Ordering::SeqCst)
|
||||
{
|
||||
if let Ok(mut q) = events_q.lock() {
|
||||
q.push_back(TurnEvent::Error(
|
||||
"Turn aborted by user".to_string(),
|
||||
));
|
||||
}
|
||||
push_event(&events_q, TurnEvent::Error(
|
||||
"Turn aborted by user".to_string(),
|
||||
));
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
@@ -648,17 +627,13 @@ pub(super) fn run_agent_turn(
|
||||
.and_then(|v| v.as_str())
|
||||
.map(std::string::ToString::to_string);
|
||||
|
||||
{
|
||||
if let Ok(mut q) = events_q.lock() {
|
||||
q.push_back(TurnEvent::ToolResult {
|
||||
tool_call_id: tool_call.id.clone(),
|
||||
tool_name: tool_name.clone(),
|
||||
output: output.clone(),
|
||||
is_error,
|
||||
path: tool_path,
|
||||
});
|
||||
}
|
||||
}
|
||||
push_event(&events_q, TurnEvent::ToolResult {
|
||||
tool_call_id: tool_call.id.clone(),
|
||||
tool_name: tool_name.clone(),
|
||||
output: output.clone(),
|
||||
is_error,
|
||||
path: tool_path,
|
||||
});
|
||||
|
||||
let tool_msg =
|
||||
ChatMessage::tool_result(tool_call.id.clone(), output);
|
||||
@@ -668,12 +643,10 @@ pub(super) fn run_agent_turn(
|
||||
} else {
|
||||
if !content.is_empty() {
|
||||
archive_message(tc.db.as_ref(), &tc.session_id, &response);
|
||||
if let Ok(mut q) = events_q.lock() {
|
||||
if stream_started {
|
||||
q.push_back(TurnEvent::StreamDone(response.clone()));
|
||||
} else {
|
||||
q.push_back(TurnEvent::AssistantMessage(response.clone()));
|
||||
}
|
||||
if stream_started {
|
||||
push_event(&events_q, TurnEvent::StreamDone(response.clone()));
|
||||
} else {
|
||||
push_event(&events_q, TurnEvent::AssistantMessage(response.clone()));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -691,12 +664,10 @@ pub(super) fn run_agent_turn(
|
||||
if has_unfinished {
|
||||
todo_retry_count += 1;
|
||||
if todo_retry_count > MAX_TODO_RETRIES {
|
||||
if let Ok(mut q) = events_q.lock() {
|
||||
q.push_back(TurnEvent::SystemNote {
|
||||
kind: "task_retry".to_string(),
|
||||
message: format!("Giving up after {MAX_TODO_RETRIES} retries — some todo items remain unfinished. Edit todo.md manually or ask again."),
|
||||
});
|
||||
}
|
||||
push_event(&events_q, TurnEvent::SystemNote {
|
||||
kind: "task_retry".to_string(),
|
||||
message: format!("Giving up after {MAX_TODO_RETRIES} retries — some todo items remain unfinished. Edit todo.md manually or ask again."),
|
||||
});
|
||||
break;
|
||||
}
|
||||
let sys_text = format!("You stopped, but you still have unfinished tasks in todo.md (marked with '- [ ]'). You MUST continue working and use tools to finish them, or edit todo.md to mark them as done if they are finished. (Retry {todo_retry_count}/{MAX_TODO_RETRIES})");
|
||||
@@ -704,12 +675,10 @@ pub(super) fn run_agent_turn(
|
||||
let msg = ChatMessage::system(sys_text);
|
||||
archive_message(tc.db.as_ref(), &tc.session_id, &msg);
|
||||
msgs.push(msg);
|
||||
if let Ok(mut q) = events_q.lock() {
|
||||
q.push_back(TurnEvent::SystemNote {
|
||||
kind: "task_retry".to_string(),
|
||||
message: sys_text_clone,
|
||||
});
|
||||
}
|
||||
push_event(&events_q, TurnEvent::SystemNote {
|
||||
kind: "task_retry".to_string(),
|
||||
message: sys_text_clone,
|
||||
});
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -717,49 +686,52 @@ pub(super) fn run_agent_turn(
|
||||
}
|
||||
}
|
||||
|
||||
let el = zesdex_cms::infrastructure::persistence::edit_log_repo::JsonlEditLogRepository::new()
|
||||
.open(&tc.edit_log_session_dir)
|
||||
.unwrap_or_else(|_| zesdex_cms::domain::edit_log::EditLog::new());
|
||||
let final_edits = el.len();
|
||||
let total_edits_this_turn = final_edits.saturating_sub(initial_edits);
|
||||
let total_edits_this_turn = initial_edit_log.as_ref().and_then(|initial_el| {
|
||||
let initial_count = initial_el.len();
|
||||
zesdex_cms::infrastructure::persistence::edit_log_repo::JsonlEditLogRepository::new()
|
||||
.open(&tc.edit_log_session_dir)
|
||||
.ok()
|
||||
.map(|final_el| {
|
||||
let count = final_el.len().saturating_sub(initial_count);
|
||||
(count, initial_count, final_el)
|
||||
})
|
||||
});
|
||||
|
||||
if total_edits_this_turn > 0 {
|
||||
if let Ok(mut q) = events_q.lock() {
|
||||
q.push_back(TurnEvent::SystemNote {
|
||||
if let Some((total_edits_this_turn, prev_edits, el)) = &total_edits_this_turn {
|
||||
if *total_edits_this_turn > 0 {
|
||||
push_event(&events_q, TurnEvent::SystemNote {
|
||||
kind: "edits".to_string(),
|
||||
message: total_edits_this_turn.to_string(),
|
||||
});
|
||||
}
|
||||
|
||||
// Collect edited paths from the new edit log entries
|
||||
let mut bg_paths = Vec::new();
|
||||
for entry in el.entries.iter().skip(initial_edits) {
|
||||
bg_paths.push(entry.path.clone());
|
||||
}
|
||||
bg_paths.sort();
|
||||
bg_paths.dedup();
|
||||
// Collect edited paths from the new edit log entries
|
||||
let mut bg_paths = Vec::new();
|
||||
for entry in el.entries.iter().skip(*prev_edits) {
|
||||
bg_paths.push(entry.path.clone());
|
||||
}
|
||||
bg_paths.sort();
|
||||
bg_paths.dedup();
|
||||
|
||||
// ── Background auto-subagents ──
|
||||
if !bg_paths.is_empty() {
|
||||
let bg_session_dir = tc.edit_log_session_dir.clone();
|
||||
let bg_workspaces = tc.workspace_roots.clone();
|
||||
let bg_events = events_q.clone();
|
||||
let bg_abort = tc.abort_flag.clone();
|
||||
std::thread::spawn(move || {
|
||||
crate::app::subagent::auto::spawn_all_background(
|
||||
&bg_paths,
|
||||
&bg_session_dir,
|
||||
&bg_workspaces,
|
||||
&bg_events,
|
||||
bg_abort,
|
||||
);
|
||||
});
|
||||
// ── Background auto-subagents ──
|
||||
if !bg_paths.is_empty() {
|
||||
let bg_session_dir = tc.edit_log_session_dir.clone();
|
||||
let bg_workspaces = tc.workspace_roots.clone();
|
||||
let bg_events = events_q.clone();
|
||||
let bg_abort = tc.abort_flag.clone();
|
||||
std::thread::spawn(move || {
|
||||
crate::app::subagent::auto::spawn_all_background(
|
||||
&bg_paths,
|
||||
&bg_session_dir,
|
||||
&bg_workspaces,
|
||||
&bg_events,
|
||||
bg_abort,
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if let Ok(mut q) = events_q.lock() {
|
||||
q.push_back(TurnEvent::Done);
|
||||
}
|
||||
push_event(&events_q, TurnEvent::Done);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -820,55 +792,9 @@ fn execute_one_tool(
|
||||
}
|
||||
let result = tool.run(ctx, args)?;
|
||||
if name == "write" || name == "edit" {
|
||||
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 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 entry = zesdex_cms::domain::edit_log::EditLogEntry {
|
||||
ts: chrono::Utc::now().timestamp_millis(),
|
||||
tool: name.to_string(),
|
||||
path: path.to_string(),
|
||||
reason: reason.to_string(),
|
||||
content_sha256,
|
||||
bytes_delta,
|
||||
origin: ctx.origin.tag(),
|
||||
session_id: sess.id.to_string(),
|
||||
};
|
||||
let repo =
|
||||
zesdex_cms::infrastructure::persistence::edit_log_repo::JsonlEditLogRepository::new();
|
||||
if let Ok(mut el) = repo.open(sess.dir) {
|
||||
let _ = repo.append(sess.dir, &mut el, entry);
|
||||
}
|
||||
crate::tool::log_write_edit_tool(
|
||||
args, name, &ctx.origin.tag(), sess.dir, sess.id,
|
||||
);
|
||||
}
|
||||
return Ok(result);
|
||||
}
|
||||
@@ -876,44 +802,6 @@ fn execute_one_tool(
|
||||
anyhow::bail!("tool not found: {name}")
|
||||
}
|
||||
|
||||
/// Build an ASCII tree of the workspace directory structure for the
|
||||
/// system prompt, so the LLM can see the file layout.
|
||||
///
|
||||
/// Flow: for each root, walk using `ignore::WalkBuilder` (respecting
|
||||
/// `.gitignore` and hidden files) → prefix `[DIR]` for directories →
|
||||
/// truncate after 1000 entries.
|
||||
///
|
||||
/// Return: a formatted string with one entry per line.
|
||||
fn generate_workspace_tree(roots: &[std::path::PathBuf]) -> String {
|
||||
let mut out = String::new();
|
||||
out.push_str("Current Workspace Directory Structure:\n");
|
||||
for root in roots {
|
||||
writeln!(out, "Root: {}", root.display()).unwrap();
|
||||
let walker = ignore::WalkBuilder::new(root)
|
||||
.hidden(true)
|
||||
.git_ignore(true)
|
||||
.build();
|
||||
let mut count = 0;
|
||||
for entry in walker.flatten() {
|
||||
let path = entry.path();
|
||||
if let Ok(rel) = path.strip_prefix(root) {
|
||||
if rel.as_os_str().is_empty() {
|
||||
continue;
|
||||
}
|
||||
let is_dir = entry.file_type().is_some_and(|ft| ft.is_dir());
|
||||
let prefix = if is_dir { "[DIR] " } else { " " };
|
||||
writeln!(out, " {}{}", prefix, rel.display()).unwrap();
|
||||
count += 1;
|
||||
if count > 1000 {
|
||||
out.push_str(" ... (truncated)\n");
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
out
|
||||
}
|
||||
|
||||
/// Load all memory entries from `memory_dir` and format them as a compact
|
||||
/// section appended to the system prompt, so the AI is always aware of
|
||||
/// stored lessons and project knowledge.
|
||||
|
||||
Reference in New Issue
Block a user