- Add AppStateRest as the central state struct for managing TUI state. - Implement InputState for handling user input, autocomplete, and history. - Create MiscState to manage overlays, notifications, and editor state. - Introduce ScrollState for viewport scrolling functionality. - Develop TranscriptCache for efficient message rendering in the chat pane. - Implement SimpleAgent and SimpleWorkflowEngine for agent lifecycle management. - Add helper functions for managing effort levels and token counting. - Organize state-related modules for better maintainability and clarity.
157 lines
5.8 KiB
Rust
157 lines
5.8 KiB
Rust
//! Shared utility functions used by tool implementations: JSON argument
|
|
//! extraction, command execution, path resolution, and edit-log persistence.
|
|
|
|
use crate::utils::CastOr;
|
|
use anyhow::Result;
|
|
use serde_json::Value;
|
|
use sha2::Digest;
|
|
use std::path::PathBuf;
|
|
use tracing::{debug, info, instrument, warn};
|
|
|
|
/// Extract a required string argument from a JSON args map.
|
|
pub fn arg_str(args: &Value, name: &str) -> Result<String> {
|
|
args.get(name)
|
|
.and_then(|v| v.as_str())
|
|
.map(std::string::ToString::to_string)
|
|
.ok_or_else(|| anyhow::anyhow!("missing required argument: {name}"))
|
|
}
|
|
|
|
/// Execute a `std::process::Command` and return its combined stdout/stderr.
|
|
///
|
|
/// Flow: spawn \u{2192} collect stdout + stderr \u{2192} check exit code \u{2192} return
|
|
/// combined output or bail with the error message.
|
|
#[instrument(skip(cmd))]
|
|
pub fn execute_cmd(cmd: &mut std::process::Command) -> Result<String> {
|
|
let output = cmd
|
|
.output()
|
|
.map_err(|e| anyhow::anyhow!("command execution failed: {e}"))?;
|
|
let stdout = String::from_utf8_lossy(&output.stdout).trim().to_string();
|
|
let stderr = String::from_utf8_lossy(&output.stderr).trim().to_string();
|
|
let stdout_len = stdout.len();
|
|
let stderr_len = stderr.len();
|
|
let combined = if stderr.is_empty() {
|
|
stdout
|
|
} else {
|
|
format!("{}\n{}", stdout, stderr)
|
|
.trim()
|
|
.to_string()
|
|
};
|
|
let code = output.status.code().unwrap_or(-1);
|
|
if output.status.success() {
|
|
info!(exit_code = code, stdout_len, "command succeeded");
|
|
Ok(combined)
|
|
} else {
|
|
warn!(exit_code = code, stderr_len, "command failed");
|
|
anyhow::bail!("command failed with exit code {code}:\n{combined}")
|
|
}
|
|
}
|
|
|
|
/// Resolve a tool-supplied relative path to an absolute path within a workspace
|
|
/// root, rejecting escapes.
|
|
///
|
|
/// Flow: parse optional `[idx]` prefix \u{2192} join with workspace root \u{2192}
|
|
/// canonicalize \u{2192} verify result is inside one of the workspace roots.
|
|
#[instrument(skip(workspaces))]
|
|
pub fn resolve_path(workspaces: &[PathBuf], rel: &str) -> Result<PathBuf> {
|
|
let (ws_idx, path) = if rel.starts_with('[') {
|
|
let close = rel
|
|
.find(']')
|
|
.ok_or_else(|| anyhow::anyhow!("invalid workspace prefix"))?;
|
|
let idx: usize = rel[1..close]
|
|
.parse()
|
|
.map_err(|_| anyhow::anyhow!("invalid workspace index"))?;
|
|
(idx, &rel[close + 1..])
|
|
} else {
|
|
(0, rel)
|
|
};
|
|
let base = workspaces
|
|
.get(ws_idx)
|
|
.ok_or_else(|| anyhow::anyhow!("workspace index {ws_idx} out of range"))?;
|
|
let abs = if path.is_empty() {
|
|
base.clone()
|
|
} else {
|
|
base.join(path)
|
|
};
|
|
let canon = if let Ok(c) = abs.canonicalize() {
|
|
c
|
|
} else {
|
|
let base_canon = workspaces
|
|
.iter()
|
|
.find_map(|w| w.canonicalize().ok())
|
|
.unwrap_or_else(|| base.clone());
|
|
let mut resolved = base_canon.clone();
|
|
if let Ok(rel_components) = abs.strip_prefix(&base_canon) {
|
|
for comp in rel_components.components() {
|
|
match comp {
|
|
std::path::Component::ParentDir => {
|
|
resolved.pop();
|
|
}
|
|
std::path::Component::CurDir => {}
|
|
c => resolved.push(c),
|
|
}
|
|
}
|
|
}
|
|
resolved
|
|
};
|
|
debug!(resolved = %canon.display(), "path resolved within workspace");
|
|
if workspaces.iter().any(|w| canon.starts_with(w)) {
|
|
Ok(canon)
|
|
} else {
|
|
warn!(path = %canon.display(), rel = rel, "path is outside all workspace roots");
|
|
anyhow::bail!("path '{rel}' is outside all workspace roots")
|
|
}
|
|
}
|
|
|
|
/// After a successful write/edit tool run, compute content hash and byte
|
|
/// delta, then persist an `EditLogEntry` to the session's edit log.
|
|
///
|
|
/// Flow: extract path/content/reason from args \u{2192} compute SHA-256 of content
|
|
/// \u{2192} compute byte delta \u{2192} build `EditLogEntry` \u{2192} open repo \u{2192} append entry.
|
|
#[instrument(skip(args, session_dir))]
|
|
pub fn log_write_edit_tool(
|
|
args: &serde_json::Value,
|
|
tool_name: &str,
|
|
origin_tag: &str,
|
|
session_dir: &std::path::Path,
|
|
session_id: &str,
|
|
) {
|
|
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 = args.get("content").or_else(|| args.get("new"));
|
|
let content_str = content.and_then(|v| v.as_str()).unwrap_or("");
|
|
let content_sha256 = hex::encode(sha2::Sha256::digest(content_str.as_bytes()));
|
|
let bytes_delta = if tool_name == "write" {
|
|
content_str.len().cast_or(0i64)
|
|
} 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("");
|
|
let new_len: i64 = new.len().cast_or(0i64);
|
|
let old_len: i64 = old.len().cast_or(0i64);
|
|
(new_len - old_len).abs()
|
|
};
|
|
let entry = zesdex_domain::cms::EditLogEntry {
|
|
ts: chrono::Utc::now().timestamp_millis(),
|
|
tool: tool_name.to_string(),
|
|
path: path.to_string(),
|
|
reason: reason.to_string(),
|
|
content_sha256,
|
|
bytes_delta,
|
|
origin: origin_tag.to_string(),
|
|
session_id: session_id.to_string(),
|
|
};
|
|
use zesdex_domain::cms::repository::EditLogRepository;
|
|
let repo = crate::persistence::cms::edit_log_repo::JsonlEditLogRepository::new();
|
|
if let Ok(mut el) = repo.open(session_dir) {
|
|
let _ = repo.append(session_dir, &mut el, entry);
|
|
debug!(tool = tool_name, path = path, "edit-log entry persisted");
|
|
} else {
|
|
warn!(tool = tool_name, path = path, "failed to open edit-log repository");
|
|
}
|
|
}
|