fabro/lib/crates/fabro-agent/src/file_tracker.rs
Bryan Helmkamp 28884ae093 rename Arc to Fabro in all Rust crates, symbols, env vars, and supporting files
- 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>
2026-03-12 12:25:58 -04:00

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());
}
}