Skip to content
Open
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 6 additions & 1 deletion crates/aft/src/commands/delete_file.rs
Original file line number Diff line number Diff line change
Expand Up @@ -108,7 +108,12 @@ fn delete_one_or_dir(
}

let path = match ctx.validate_path(&req.id, requested_path) {
Ok(path) => path,
Ok(path) => {
ctx.session_history()
.borrow_mut()
.record(req.session(), path.clone(), crate::session_history::FileOp::Delete);
path
}
Err(resp) => return Err(resp),
};

Expand Down
7 changes: 6 additions & 1 deletion crates/aft/src/commands/edit_match.rs
Original file line number Diff line number Diff line change
Expand Up @@ -132,7 +132,12 @@ fn handle_append(req: &RawRequest, ctx: &AppContext, op_id: &str) -> Response {
.unwrap_or(true);

let path = match ctx.validate_path(&req.id, Path::new(file)) {
Ok(path) => path,
Ok(path) => {
ctx.session_history()
.borrow_mut()
.record(req.session(), path.clone(), crate::session_history::FileOp::Edit);
path
}
Err(resp) => return resp,
};

Expand Down
7 changes: 6 additions & 1 deletion crates/aft/src/commands/edit_symbol.rs
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,12 @@ pub fn handle_edit_symbol(req: &RawRequest, ctx: &AppContext) -> Response {
}

let path = match ctx.validate_path(&req.id, Path::new(file)) {
Ok(path) => path,
Ok(path) => {
ctx.session_history()
.borrow_mut()
.record(req.session(), path.clone(), crate::session_history::FileOp::Edit);
path
}
Err(resp) => return resp,
};
if !path.exists() {
Expand Down
7 changes: 6 additions & 1 deletion crates/aft/src/commands/move_file.rs
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,12 @@ pub fn handle_move_file(req: &RawRequest, ctx: &AppContext) -> Response {
};

let src_path = match ctx.validate_path(&req.id, Path::new(file)) {
Ok(path) => path,
Ok(path) => {
ctx.session_history()
.borrow_mut()
.record(req.session(), path.clone(), crate::session_history::FileOp::Move);
path
}
Err(resp) => return resp,
};
let dst_path = match ctx.validate_path(&req.id, Path::new(destination)) {
Expand Down
44 changes: 42 additions & 2 deletions crates/aft/src/commands/outline.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
use std::collections::HashMap;
use std::io::Read as _;
use std::path::{Path, PathBuf};
use std::process::Command;
use std::sync::Arc;
use std::time::UNIX_EPOCH;

Expand Down Expand Up @@ -289,6 +291,7 @@ struct OutlineFileEntry {
language: String,
symbols: usize,
bytes: u64,
git_status: String,
}

#[derive(Debug, Clone)]
Expand Down Expand Up @@ -347,13 +350,14 @@ fn handle_outline_files_mode(
} else {
&dir_path
};
let git_statuses = collect_git_statuses(&dir_path);
let discovery = discover_outline_files_for_files_mode(&dir_path, ctx);
walk_truncated |= discovery.walk_truncated;
collection_truncated |= discovery.collection_truncated;

for file in discovery.files {
let file_path = PathBuf::from(file);
if let Some(entry) = outline_file_entry(&file_path, display_root, ctx) {
if let Some(entry) = outline_file_entry(&file_path, display_root, &git_statuses, ctx) {
file_entries.push(entry);
}
}
Expand Down Expand Up @@ -461,23 +465,54 @@ fn discover_outline_files_for_files_mode(
discover_outline_files_with_options(directory, Some(&options))
}

fn collect_git_statuses(dir: &Path) -> HashMap<String, String> {
Comment thread
cubic-dev-ai[bot] marked this conversation as resolved.
let output = match Command::new("git")
.args([
"-C",
&dir.to_string_lossy(),
"status",
"--porcelain",
])
.output()
{
Ok(o) if o.status.success() => o,
_ => return HashMap::new(),
};
let stdout = String::from_utf8_lossy(&output.stdout);
let mut map = HashMap::new();
for line in stdout.lines() {
if line.len() >= 4 {
let status = line[..2].to_string();
let path = line[3..].to_string();
map.insert(path, status);
}
}
map
}

fn outline_file_entry(
path: &Path,
display_root: &Path,
git_statuses: &HashMap<String, String>,
ctx: &AppContext,
) -> Option<OutlineFileEntry> {
let metadata = std::fs::metadata(path).ok()?;
let rel_path =
relative_path_from_root(path, display_root).unwrap_or_else(|| path_to_slash(path));
let bytes = metadata.len();
let detected_language = detect_language(path).map(language_id);
let git_status = git_statuses
.get(&rel_path)
.cloned()
.unwrap_or_else(|| " ".to_string());

if let Some(symbols) = cached_symbol_count(ctx, path, &metadata) {
return Some(OutlineFileEntry {
path: rel_path,
language: detected_language.unwrap_or("unknown").to_string(),
symbols,
bytes,
git_status,
});
}

Expand All @@ -492,6 +527,7 @@ fn outline_file_entry(
.to_string(),
symbols: 0,
bytes,
git_status,
});
};

Expand All @@ -501,6 +537,7 @@ fn outline_file_entry(
language: "binary".to_string(),
symbols: 0,
bytes,
git_status,
});
}

Expand All @@ -510,6 +547,7 @@ fn outline_file_entry(
language: language.to_string(),
symbols: 0,
bytes,
git_status,
});
}

Expand All @@ -524,6 +562,7 @@ fn outline_file_entry(
language: language.to_string(),
symbols,
bytes,
git_status,
})
}

Expand Down Expand Up @@ -620,7 +659,8 @@ fn format_files_table(

for entry in entries {
let line = format!(
"{:<path_width$} {:<language_width$} {:>5} syms {:>9} bytes\n",
"{} {:<path_width$} {:<language_width$} {:>5} syms {:>9} bytes\n",
entry.git_status,
entry.path,
entry.language,
entry.symbols,
Expand Down
7 changes: 6 additions & 1 deletion crates/aft/src/commands/read.rs
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,12 @@ pub fn handle_read(req: &RawRequest, ctx: &AppContext) -> Response {
};

let path = match ctx.validate_path(&req.id, Path::new(file)) {
Ok(path) => path,
Ok(path) => {
ctx.session_history()
.borrow_mut()
.record(req.session(), path.clone(), crate::session_history::FileOp::Read);
path
}
Err(resp) => return resp,
};

Expand Down
7 changes: 6 additions & 1 deletion crates/aft/src/commands/write.rs
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,12 @@ pub fn handle_write(req: &RawRequest, ctx: &AppContext) -> Response {
.unwrap_or(true);

let path = match ctx.validate_path(&req.id, Path::new(file)) {
Ok(path) => path,
Ok(path) => {
ctx.session_history()
.borrow_mut()
.record(req.session(), path.clone(), crate::session_history::FileOp::Write);
path
}
Err(resp) => return resp,
};
let existed = path.exists();
Expand Down
7 changes: 6 additions & 1 deletion crates/aft/src/commands/zoom.rs
Original file line number Diff line number Diff line change
Expand Up @@ -110,7 +110,12 @@ pub fn handle_zoom(req: &RawRequest, ctx: &AppContext) -> Response {
.map(|v| v as usize);

let path = match resolve_file_or_url(req, ctx, file) {
Ok(path) => path,
Ok(path) => {
ctx.session_history()
.borrow_mut()
.record(req.session(), path.clone(), crate::session_history::FileOp::Zoom);
path
}
Err(resp) => return resp,
};
if !path.exists() {
Expand Down
54 changes: 54 additions & 0 deletions crates/aft/src/context.rs
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ use crate::parser::{SharedSymbolCache, SymbolCache};
use crate::protocol::{
ConfigureWarningsFrame, ProgressFrame, PushFrame, StatusChangedFrame, StatusPayload,
};
use crate::session_history::{FileOp, SessionHistory};

pub type ProgressSender = Arc<Box<dyn Fn(PushFrame) + Send + Sync>>;
pub type SharedProgressSender = Arc<Mutex<Option<ProgressSender>>>;
Expand Down Expand Up @@ -379,6 +380,11 @@ pub struct AppContext {
/// root is configured or when the project has no gitignore files; in that
/// case the watcher falls back to a small hardcoded infra-directory skip.
gitignore: RefCell<Option<Arc<ignore::gitignore::Gitignore>>>,
/// Per-session, in-memory, chronologically-ordered file-access history.
/// Records every `read`, `zoom`, `edit`, `write`, `delete`, and `move`
/// operation so the agent (or user) can answer "what files was I just
/// working on?" within the current session.
session_history: RefCell<SessionHistory>,
}

impl AppContext {
Expand Down Expand Up @@ -432,6 +438,7 @@ impl AppContext {
filter_registry_loaded: std::sync::atomic::AtomicBool::new(false),
bash_compress_flag: Arc::new(std::sync::atomic::AtomicBool::new(bash_compress_enabled)),
gitignore: RefCell::new(None),
session_history: RefCell::new(SessionHistory::new()),
}
}

Expand Down Expand Up @@ -465,6 +472,53 @@ impl AppContext {
*self.gitignore.borrow_mut() = None;
}

/// Access the per-session file-access history store.
pub fn session_history(&self) -> &RefCell<SessionHistory> {
&self.session_history
}

/// Record a file read operation in session history.
pub fn record_file_read(&self, session: &str, path: &std::path::Path) {
self.session_history
.borrow_mut()
.record(session, path.to_path_buf(), FileOp::Read);
}

/// Record a file zoom operation in session history.
pub fn record_file_zoom(&self, session: &str, path: &std::path::Path) {
self.session_history
.borrow_mut()
.record(session, path.to_path_buf(), FileOp::Zoom);
}

/// Record a file edit operation in session history.
pub fn record_file_edit(&self, session: &str, path: &std::path::Path) {
self.session_history
.borrow_mut()
.record(session, path.to_path_buf(), FileOp::Edit);
}

/// Record a file write operation in session history.
pub fn record_file_write(&self, session: &str, path: &std::path::Path) {
self.session_history
.borrow_mut()
.record(session, path.to_path_buf(), FileOp::Write);
}

/// Record a file delete operation in session history.
pub fn record_file_delete(&self, session: &str, path: &std::path::Path) {
self.session_history
.borrow_mut()
.record(session, path.to_path_buf(), FileOp::Delete);
}

/// Record a file move operation in session history.
pub fn record_file_move(&self, session: &str, path: &std::path::Path) {
self.session_history
.borrow_mut()
.record(session, path.to_path_buf(), FileOp::Move);
}

pub fn rebuild_gitignore(&self) {
use ignore::gitignore::GitignoreBuilder;
use std::path::Path;
Expand Down
1 change: 1 addition & 0 deletions crates/aft/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,7 @@ pub mod protocol;
pub mod query_shape;
pub mod search_index;
pub mod semantic_index;
pub mod session_history;
pub mod symbol_cache_disk;
pub mod symbols;
pub mod url_fetch;
Expand Down
19 changes: 19 additions & 0 deletions crates/aft/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -452,6 +452,7 @@ fn dispatch(req: RawRequest, ctx: &AppContext) -> Response {
"grep" => aft::commands::grep::handle_grep(&req, ctx),
"semantic_search" => aft::commands::semantic_search::handle_semantic_search(&req, ctx),
"status" => aft::commands::status::handle_status(&req, ctx),
"session_history" => handle_session_history(&req, ctx),
"list_filters" => aft::commands::list_filters::handle_list_filters(&req, ctx),
"trust_filter_project" => {
aft::commands::trust_filter_project::handle_trust_filter_project(&req, ctx)
Expand Down Expand Up @@ -541,6 +542,24 @@ fn handle_snapshot(req: &RawRequest, ctx: &AppContext) -> Response {
}
}

fn handle_session_history(req: &RawRequest, ctx: &AppContext) -> Response {
let limit = req
.params
.get("limit")
.and_then(|v| v.as_u64())
.unwrap_or(50)
.min(200) as usize;
let session = req.session();
let entries = ctx.session_history().borrow().recent(session, limit);
Response::success(
&req.id,
serde_json::json!({
"entries": entries,
"count": entries.len(),
}),
)
}

fn write_response(ctx: &AppContext, response: &Response) -> io::Result<()> {
let stdout_writer = ctx.stdout_writer();
let mut writer = stdout_writer
Expand Down
Loading