Skip to content
Draft
Show file tree
Hide file tree
Changes from all 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
794 changes: 234 additions & 560 deletions app/src/ai/blocklist/action_model.rs

Large diffs are not rendered by default.

1,072 changes: 775 additions & 297 deletions app/src/ai/blocklist/action_model/execute.rs

Large diffs are not rendered by default.

195 changes: 4 additions & 191 deletions app/src/ai/blocklist/action_model/execute/file_glob.rs
Original file line number Diff line number Diff line change
@@ -1,200 +1,17 @@
use std::path::PathBuf;
use std::sync::Arc;
use std::time::Duration;

use futures::future::BoxFuture;
use futures::FutureExt;
use itertools::Itertools;
use warp_core::features::FeatureFlag;
use warpui::r#async::FutureExt as AsyncFutureExt;
use warpui::{AppContext, Entity, EntityId, ModelContext, ModelHandle, SingletonEntity};

use crate::ai::agent::conversation::AIConversationId;
use crate::ai::agent::{
AIAgentAction, AIAgentActionResultType, AIAgentActionType, FileGlobResult, FileGlobV2Match,
FileGlobV2Result,
};
use crate::ai::blocklist::BlocklistAIPermissions;
use crate::ai::paths::{host_native_absolute_path, join_paths, shell_native_absolute_path};
use crate::terminal::model::session::active_session::ActiveSession;
use super::is_git_repository;
use crate::ai::agent::{FileGlobV2Match, FileGlobV2Result};
use crate::ai::paths::join_paths;
use crate::terminal::model::session::command_executor::shell_quote_arg;
use crate::terminal::model::session::{ExecuteCommandOptions, Session};
use crate::terminal::shell::ShellType;
use crate::terminal::ShellLaunchData;
use crate::{send_telemetry_from_app_ctx, TelemetryEvent};

const FILE_GLOB_TIMEOUT: Duration = Duration::from_secs(10);

use super::{
get_server_output_id, is_git_repository, ActionExecution, AnyActionExecution,
ExecuteActionInput, PreprocessActionInput,
};

pub struct FileGlobExecutor {
active_session: ModelHandle<ActiveSession>,
terminal_view_id: EntityId,
}

fn log_file_glob_error(conversation_id: AIConversationId, ctx: &mut AppContext) {
let server_output_id = get_server_output_id(conversation_id, ctx);
send_telemetry_from_app_ctx!(TelemetryEvent::FileGlobToolFailed { server_output_id }, ctx);
}

impl FileGlobExecutor {
pub fn new(active_session: ModelHandle<ActiveSession>, terminal_view_id: EntityId) -> Self {
Self {
active_session,
terminal_view_id,
}
}

pub(super) fn should_autoexecute(
&self,
input: ExecuteActionInput,
ctx: &mut ModelContext<Self>,
) -> bool {
let ExecuteActionInput {
action:
AIAgentAction {
action:
AIAgentActionType::FileGlob { path, .. }
| AIAgentActionType::FileGlobV2 {
search_dir: path, ..
},
..
},
conversation_id,
} = input
else {
return false;
};

// If the path is not provided, use the current working directory.
let path = path.clone().unwrap_or_else(|| ".".to_string());

let current_working_directory = self
.active_session
.as_ref(ctx)
.current_working_directory()
.cloned();
let shell = self.active_session.as_ref(ctx).shell_launch_data(ctx);
let absolute_path =
host_native_absolute_path(path.as_str(), &shell, &current_working_directory);

BlocklistAIPermissions::as_ref(ctx)
.can_read_files_with_conversation(
&conversation_id,
vec![PathBuf::from(absolute_path)],
Some(self.terminal_view_id),
ctx,
)
.is_allowed()
}

pub(super) fn execute(
&mut self,
input: ExecuteActionInput,
ctx: &mut ModelContext<Self>,
) -> impl Into<AnyActionExecution> {
let AIAgentAction {
action:
AIAgentActionType::FileGlob { patterns, path }
| AIAgentActionType::FileGlobV2 {
patterns,
search_dir: path,
},
..
} = input.action
else {
return ActionExecution::InvalidAction;
};

// If the path is not provided, use the current working directory.
let path = path.clone().unwrap_or_else(|| ".".to_string());

let shell_launch_data = self.active_session.as_ref(ctx).shell_launch_data(ctx);
let current_working_directory = self
.active_session
.as_ref(ctx)
.current_working_directory()
.cloned();
let absolute_path = shell_native_absolute_path(
path.as_str(),
shell_launch_data.as_ref(),
current_working_directory.as_ref(),
);

let session = self.active_session.as_ref(ctx).session(ctx);

let patterns_clone = patterns.clone();
let conversation_id_clone = input.conversation_id;
let is_file_glob_v2 = is_file_glob_v2(&input);
ActionExecution::new_async(
async move {
match run_file_glob(patterns_clone, absolute_path, session, shell_launch_data)
.with_timeout(FILE_GLOB_TIMEOUT)
.await
{
Ok(result) => result,
Err(_) => Err(anyhow::anyhow!("File glob operation timed out")),
}
},
move |result, ctx| match result {
Ok(file_glob_result) => {
match file_glob_result {
FileGlobV2Result::Error(ref e) => {
log::warn!("Executing file_glob resulted in error: {e:?}");
log_file_glob_error(conversation_id_clone, ctx);
}
FileGlobV2Result::Success { .. } => {
send_telemetry_from_app_ctx!(
TelemetryEvent::FileGlobToolSucceeded,
ctx
);
}
_ => {}
}
// Convert FileGlobV2Result to FileGlobResult if the request was not V2.
if is_file_glob_v2 {
AIAgentActionResultType::FileGlobV2(file_glob_result)
} else {
AIAgentActionResultType::FileGlob(file_glob_result.into())
}
}
Err(e) => {
log::warn!("Failed to execute file_glob: {e:?}");
log_file_glob_error(conversation_id_clone, ctx);
if is_file_glob_v2 {
AIAgentActionResultType::FileGlobV2(FileGlobV2Result::Error(e.to_string()))
} else {
AIAgentActionResultType::FileGlob(FileGlobResult::Error(e.to_string()))
}
}
},
)
}

pub(super) fn preprocess_action(
&mut self,
_action: PreprocessActionInput,
_ctx: &mut ModelContext<Self>,
) -> BoxFuture<'static, ()> {
futures::future::ready(()).boxed()
}

pub(super) fn can_execute_in_parallel(&self, ctx: &AppContext) -> bool {
self.active_session
.as_ref(ctx)
.session(ctx)
.is_some_and(|session| session.supports_parallel_command_execution())
}
}

fn is_file_glob_v2(input: &ExecuteActionInput) -> bool {
matches!(input.action.action, AIAgentActionType::FileGlobV2 { .. })
}

async fn run_file_glob(
pub(crate) async fn run_file_glob(
patterns: Vec<String>,
absolute_path: String,
session: Option<Arc<Session>>,
Expand Down Expand Up @@ -398,10 +215,6 @@ fn non_empty_lines(str: &str) -> impl Iterator<Item = &str> {
str.lines().filter(|line| !line.is_empty())
}

impl Entity for FileGlobExecutor {
type Event = ();
}

#[cfg(test)]
#[path = "file_glob_tests.rs"]
mod tests;
Loading