mirror of
https://github.com/fabro-sh/fabro.git
synced 2026-10-07 03:00:29 +00:00
- Rename 20 crate directories lib/crates/arc-* → fabro-* - Update all Cargo.toml: crate names, dep paths, feature flags, bin name - Rename arc_server module → fabro_server in fabro-llm - ArcError → FabroError across 30+ files - ARC_VERSION/ARC_GIT_SHA/ARC_BUILD_DATE → FABRO_* constants - All use/qualified paths: arc_agent:: → fabro_agent::, etc. (~1500 occurrences) - Env vars ARC_* → FABRO_* in string literals and shell scripts - String literals: X-Arc-Demo, arc-bot, arc@local, arc-web, arc-mcp, etc. - Path strings: .arc/ → .fabro/, arc.toml → fabro.toml, refs/arc/ → refs/fabro/ - arc-api.yaml → fabro-api.yaml (OpenAPI spec) - skills/arc-create-workflow → fabro-create-workflow - trycmd fixtures: $ arc → $ fabro - Inline snapshots (insta) updated - CI, Docker, install.sh, scripts, CLAUDE.md, AGENTS.md - TypeScript app: env vars, headers, JWT issuer - Docs: page slugs, git refs, config paths, sandbox names, repo URLs - Repo references: brynary/arc → fabro-sh/fabro Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
226 lines
7.2 KiB
Rust
226 lines
7.2 KiB
Rust
use fabro_llm::types::{ToolCall, ToolResult};
|
|
use std::collections::BTreeMap;
|
|
|
|
#[derive(Debug, Clone, Copy, Default)]
|
|
struct FileOps {
|
|
read: bool,
|
|
written: bool,
|
|
edited: bool,
|
|
}
|
|
|
|
#[derive(Debug, Default)]
|
|
pub struct FileTracker {
|
|
files: BTreeMap<String, FileOps>,
|
|
}
|
|
|
|
impl FileTracker {
|
|
pub fn record_read(&mut self, path: &str) {
|
|
self.files.entry(path.to_string()).or_default().read = true;
|
|
}
|
|
|
|
pub fn record_write(&mut self, path: &str) {
|
|
self.files.entry(path.to_string()).or_default().written = true;
|
|
}
|
|
|
|
pub fn record_edit(&mut self, path: &str) {
|
|
self.files.entry(path.to_string()).or_default().edited = true;
|
|
}
|
|
|
|
pub fn is_empty(&self) -> bool {
|
|
self.files.is_empty()
|
|
}
|
|
|
|
pub fn file_count(&self) -> usize {
|
|
self.files.len()
|
|
}
|
|
|
|
pub fn render(&self) -> String {
|
|
let mut output = String::new();
|
|
for (path, ops) in &self.files {
|
|
let mut labels = Vec::new();
|
|
if ops.read {
|
|
labels.push("read");
|
|
}
|
|
if ops.written {
|
|
labels.push("written");
|
|
}
|
|
if ops.edited {
|
|
labels.push("edited");
|
|
}
|
|
output.push_str(&format!("- {path} ({})\n", labels.join(", ")));
|
|
}
|
|
output
|
|
}
|
|
|
|
pub fn record_from_tool_calls(&mut self, tool_calls: &[ToolCall], results: &[ToolResult]) {
|
|
for (tc, result) in tool_calls.iter().zip(results.iter()) {
|
|
if result.is_error {
|
|
continue;
|
|
}
|
|
match tc.name.as_str() {
|
|
"read_file" => {
|
|
if let Some(path) = tc.arguments.get("file_path").and_then(|v| v.as_str()) {
|
|
self.record_read(path);
|
|
}
|
|
}
|
|
"write_file" => {
|
|
if let Some(path) = tc.arguments.get("file_path").and_then(|v| v.as_str()) {
|
|
self.record_write(path);
|
|
}
|
|
}
|
|
"edit_file" => {
|
|
if let Some(path) = tc.arguments.get("file_path").and_then(|v| v.as_str()) {
|
|
self.record_edit(path);
|
|
}
|
|
}
|
|
"apply_patch" => {
|
|
let content = match result.content.as_str() {
|
|
Some(s) => s.to_string(),
|
|
None => result.content.to_string(),
|
|
};
|
|
for line in content.lines() {
|
|
let line = line.trim();
|
|
if let Some(path) = line.strip_prefix("Added file: ") {
|
|
self.record_write(path.trim());
|
|
} else if let Some(path) = line.strip_prefix("Updated file: ") {
|
|
self.record_edit(path.trim());
|
|
}
|
|
}
|
|
}
|
|
_ => {}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
|
|
#[test]
|
|
fn record_read_renders_read_flag() {
|
|
let mut tracker = FileTracker::default();
|
|
tracker.record_read("src/main.rs");
|
|
assert_eq!(tracker.render(), "- src/main.rs (read)\n");
|
|
}
|
|
|
|
#[test]
|
|
fn record_write_and_edit_renders_all_ops() {
|
|
let mut tracker = FileTracker::default();
|
|
tracker.record_read("src/lib.rs");
|
|
tracker.record_write("src/lib.rs");
|
|
tracker.record_edit("src/lib.rs");
|
|
assert_eq!(tracker.render(), "- src/lib.rs (read, written, edited)\n");
|
|
}
|
|
|
|
#[test]
|
|
fn multiple_files_sorted_by_path() {
|
|
let mut tracker = FileTracker::default();
|
|
tracker.record_write("z.rs");
|
|
tracker.record_read("a.rs");
|
|
let rendered = tracker.render();
|
|
assert_eq!(rendered, "- a.rs (read)\n- z.rs (written)\n");
|
|
}
|
|
|
|
#[test]
|
|
fn record_from_tool_calls_read_file() {
|
|
let mut tracker = FileTracker::default();
|
|
let tool_calls = vec![ToolCall::new(
|
|
"tc1",
|
|
"read_file",
|
|
serde_json::json!({"file_path": "/tmp/foo.rs"}),
|
|
)];
|
|
let results = vec![ToolResult::success(
|
|
"tc1",
|
|
serde_json::json!("file contents"),
|
|
)];
|
|
tracker.record_from_tool_calls(&tool_calls, &results);
|
|
assert_eq!(tracker.render(), "- /tmp/foo.rs (read)\n");
|
|
}
|
|
|
|
#[test]
|
|
fn record_from_tool_calls_write_file() {
|
|
let mut tracker = FileTracker::default();
|
|
let tool_calls = vec![ToolCall::new(
|
|
"tc1",
|
|
"write_file",
|
|
serde_json::json!({"file_path": "/tmp/bar.rs", "content": "hello"}),
|
|
)];
|
|
let results = vec![ToolResult::success("tc1", serde_json::json!("ok"))];
|
|
tracker.record_from_tool_calls(&tool_calls, &results);
|
|
assert_eq!(tracker.render(), "- /tmp/bar.rs (written)\n");
|
|
}
|
|
|
|
#[test]
|
|
fn record_from_tool_calls_edit_file() {
|
|
let mut tracker = FileTracker::default();
|
|
let tool_calls = vec![ToolCall::new(
|
|
"tc1",
|
|
"edit_file",
|
|
serde_json::json!({"file_path": "/tmp/baz.rs"}),
|
|
)];
|
|
let results = vec![ToolResult::success("tc1", serde_json::json!("ok"))];
|
|
tracker.record_from_tool_calls(&tool_calls, &results);
|
|
assert_eq!(tracker.render(), "- /tmp/baz.rs (edited)\n");
|
|
}
|
|
|
|
#[test]
|
|
fn record_from_tool_calls_skips_errors() {
|
|
let mut tracker = FileTracker::default();
|
|
let tool_calls = vec![ToolCall::new(
|
|
"tc1",
|
|
"read_file",
|
|
serde_json::json!({"file_path": "/tmp/missing.rs"}),
|
|
)];
|
|
let results = vec![ToolResult::error("tc1", "File not found")];
|
|
tracker.record_from_tool_calls(&tool_calls, &results);
|
|
assert!(tracker.is_empty());
|
|
}
|
|
|
|
#[test]
|
|
fn record_from_tool_calls_apply_patch_added() {
|
|
let mut tracker = FileTracker::default();
|
|
let tool_calls = vec![ToolCall::new(
|
|
"tc1",
|
|
"apply_patch",
|
|
serde_json::json!({"patch": "..."}),
|
|
)];
|
|
let results = vec![ToolResult::success(
|
|
"tc1",
|
|
serde_json::json!("Added file: src/new.rs\nUpdated file: src/old.rs"),
|
|
)];
|
|
tracker.record_from_tool_calls(&tool_calls, &results);
|
|
assert_eq!(
|
|
tracker.render(),
|
|
"- src/new.rs (written)\n- src/old.rs (edited)\n"
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn is_empty_and_file_count() {
|
|
let mut tracker = FileTracker::default();
|
|
assert!(tracker.is_empty());
|
|
assert_eq!(tracker.file_count(), 0);
|
|
|
|
tracker.record_read("a.rs");
|
|
tracker.record_write("b.rs");
|
|
assert!(!tracker.is_empty());
|
|
assert_eq!(tracker.file_count(), 2);
|
|
}
|
|
|
|
#[test]
|
|
fn record_from_tool_calls_ignores_unknown_tools() {
|
|
let mut tracker = FileTracker::default();
|
|
let tool_calls = vec![ToolCall::new(
|
|
"tc1",
|
|
"shell",
|
|
serde_json::json!({"command": "ls"}),
|
|
)];
|
|
let results = vec![ToolResult::success(
|
|
"tc1",
|
|
serde_json::json!("file1\nfile2"),
|
|
)];
|
|
tracker.record_from_tool_calls(&tool_calls, &results);
|
|
assert!(tracker.is_empty());
|
|
}
|
|
}
|