Files
galaxy/app/src/terminal/input/conversations/data_source.rs
T

142 lines
5.3 KiB
Rust

//! Data source for the inline conversation menu.
use galaxyui::{AppContext, Entity, ModelHandle};
use itertools::Itertools;
use ordered_float::OrderedFloat;
use crate::ai::blocklist::agent_view::AgentViewController;
use crate::ai::conversation_navigation::ConversationNavigationData;
use crate::search::data_source::{Query, QueryFilter, QueryResult};
use crate::search::mixer::DataSourceRunErrorWrapper;
use crate::search::SyncDataSource;
use crate::terminal::input::conversations::search_item::ConversationSearchItem;
use crate::terminal::input::conversations::AcceptConversation;
use crate::terminal::model::session::active_session::ActiveSession;
pub struct ConversationMenuDataSource {
agent_view_controller: ModelHandle<AgentViewController>,
active_session: ModelHandle<ActiveSession>,
}
impl ConversationMenuDataSource {
pub fn new(
agent_view_controller: ModelHandle<AgentViewController>,
active_session: ModelHandle<ActiveSession>,
) -> Self {
Self {
agent_view_controller,
active_session,
}
}
}
impl SyncDataSource for ConversationMenuDataSource {
type Action = AcceptConversation;
fn run_query(
&self,
query: &Query,
app: &AppContext,
) -> Result<Vec<QueryResult<Self::Action>>, DataSourceRunErrorWrapper> {
let conversation_navigation_data = ConversationNavigationData::all_conversations(app);
let query_text = query.text.trim().to_lowercase();
let active_conversation_id = self
.agent_view_controller
.as_ref(app)
.agent_view_state()
.active_conversation_id();
let filter_by_cwd = query
.filters
.contains(&QueryFilter::CurrentDirectoryConversations);
let session_pwd = if filter_by_cwd {
self.active_session
.as_ref(app)
.current_working_directory()
.cloned()
} else {
None
};
// When the "Current Directory" filter is active, include only conversations
// whose most recent directory (falling back to initial directory) matches
// the session's current working directory. If we can't determine the
// session CWD, leave the results unfiltered.
let matches_directory = |data: &ConversationNavigationData| -> bool {
if !filter_by_cwd {
return true;
}
let Some(session_pwd) = session_pwd.as_deref() else {
return true;
};
data.latest_working_directory
.as_deref()
.or(data.initial_working_directory.as_deref())
.is_some_and(|dir| {
dir.trim_end_matches(std::path::MAIN_SEPARATOR)
== session_pwd.trim_end_matches(std::path::MAIN_SEPARATOR)
})
};
if query_text.is_empty() {
// By default, show 50 most recent conversations in the list.
const DEFAULT_RESULT_COUNT: usize = 50;
// In the zero state, sort conversations in the active pane above all other conversations.
// Within each segment, sort to reverse chronological order.
Ok(conversation_navigation_data
.into_iter()
// Don't show the currently open conversation, that's redundant.
.filter(|data| Some(data.id()) != active_conversation_id)
.filter(|data| matches_directory(data))
.sorted_by(|a, b| b.last_updated.cmp(&a.last_updated))
.take(DEFAULT_RESULT_COUNT)
.map(|navigation_data| {
QueryResult::from(ConversationSearchItem::new(navigation_data, app))
})
.rev()
.collect())
} else {
let mut search_results = conversation_navigation_data
.into_iter()
.filter_map(|navigation_data| {
if Some(navigation_data.id()) == active_conversation_id {
// Don't show the currently open conversation, that's redundant.
return None;
}
if !matches_directory(&navigation_data) {
return None;
}
let match_result = fuzzy_match::match_indices_case_insensitive(
&navigation_data.title,
&query_text,
)?;
// 25 is arbitrary.
if match_result.score < 25 {
return None;
}
Some(QueryResult::from(
ConversationSearchItem::new(navigation_data, app)
.with_name_match_result(Some(match_result.clone()))
.with_score(OrderedFloat(match_result.score as f64)),
))
})
.sorted_by(|a, b| b.score().cmp(&a.score()))
.collect_vec();
// This is basically here so the app doesn't choke.
const MAX_SEARCH_RESULTS: usize = 500;
search_results.truncate(MAX_SEARCH_RESULTS);
Ok(search_results)
}
}
}
impl Entity for ConversationMenuDataSource {
type Event = ();
}