Initial public release of Warp.
Repo-Sync-Origin: warpdotdev/warp-internal@12af1d983b
This commit is contained in:
@@ -0,0 +1,141 @@
|
||||
//! Data source for the inline conversation menu.
|
||||
|
||||
use itertools::Itertools;
|
||||
use ordered_float::OrderedFloat;
|
||||
use warpui::{AppContext, Entity, ModelHandle};
|
||||
|
||||
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 = ();
|
||||
}
|
||||
@@ -0,0 +1,101 @@
|
||||
//! Inline conversation menu for selecting AI conversations, enabled
|
||||
//! when `FeatureFlag::AgentView` is enabled.
|
||||
mod data_source;
|
||||
mod search_item;
|
||||
mod view;
|
||||
|
||||
pub use view::{InlineConversationMenuEvent, InlineConversationMenuView};
|
||||
|
||||
use pathfinder_color::ColorU;
|
||||
use warp_core::ui::appearance::Appearance;
|
||||
use warpui::{keymap::Keystroke, SingletonEntity};
|
||||
|
||||
use crate::ai::active_agent_views_model::{ActiveAgentViewsModel, ConversationOrTaskId};
|
||||
use crate::ai::conversation_navigation::ConversationNavigationData;
|
||||
use crate::terminal::input::inline_menu::{
|
||||
default_navigation_message_items, InlineMenuAction, InlineMenuMessageArgs, InlineMenuRowAction,
|
||||
InlineMenuType,
|
||||
};
|
||||
use crate::terminal::input::message_bar::{Message, MessageItem};
|
||||
|
||||
/// Tab identifiers for the inline conversation menu.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum InlineConversationMenuTab {
|
||||
/// Show all conversations.
|
||||
All,
|
||||
/// Show only conversations whose most recent directory matches the session's CWD.
|
||||
CurrentDirectory,
|
||||
}
|
||||
|
||||
/// Action emitted when enter is hit on a conversation the inline conversation menu.
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct AcceptConversation {
|
||||
pub navigation_data: ConversationNavigationData,
|
||||
}
|
||||
|
||||
impl InlineMenuAction for AcceptConversation {
|
||||
const MENU_TYPE: InlineMenuType = InlineMenuType::ConversationMenu;
|
||||
|
||||
fn produce_inline_menu_message<T>(args: InlineMenuMessageArgs<'_, Self, T>) -> Option<Message> {
|
||||
let InlineMenuMessageArgs {
|
||||
inline_menu_model,
|
||||
app,
|
||||
} = args;
|
||||
|
||||
let mut items = Vec::new();
|
||||
|
||||
if let Some(item) = inline_menu_model.selected_item() {
|
||||
let data = &item.navigation_data;
|
||||
|
||||
let active_ids =
|
||||
ActiveAgentViewsModel::as_ref(app).get_all_active_conversation_ids(app);
|
||||
let is_active = active_ids.contains(&ConversationOrTaskId::ConversationId(data.id));
|
||||
|
||||
let text = if is_active {
|
||||
" go to conversation"
|
||||
} else {
|
||||
" continue in this pane"
|
||||
};
|
||||
|
||||
let navigation_data = data.clone();
|
||||
items.push(MessageItem::clickable(
|
||||
vec![
|
||||
MessageItem::keystroke(Keystroke {
|
||||
key: "enter".to_owned(),
|
||||
..Default::default()
|
||||
}),
|
||||
MessageItem::text(text),
|
||||
],
|
||||
move |ctx| {
|
||||
ctx.dispatch_typed_action(InlineMenuRowAction::Accept {
|
||||
item: AcceptConversation {
|
||||
navigation_data: navigation_data.clone(),
|
||||
},
|
||||
cmd_or_ctrl_enter: false,
|
||||
});
|
||||
},
|
||||
inline_menu_model.mouse_states().accept.clone(),
|
||||
));
|
||||
} else {
|
||||
let theme = Appearance::as_ref(app).theme();
|
||||
let disabled_color = theme.disabled_text_color(theme.background()).into_solid();
|
||||
items.extend([
|
||||
MessageItem::Keystroke {
|
||||
keystroke: Keystroke {
|
||||
key: "enter".to_owned(),
|
||||
..Default::default()
|
||||
},
|
||||
color: Some(disabled_color),
|
||||
background_color: Some(ColorU::transparent_black()),
|
||||
},
|
||||
MessageItem::Text {
|
||||
content: " continue in this pane".into(),
|
||||
color: Some(disabled_color),
|
||||
},
|
||||
]);
|
||||
}
|
||||
|
||||
items.extend(default_navigation_message_items(&args));
|
||||
Some(Message::new(items))
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,205 @@
|
||||
//! SearchItem implementation for conversation menu items.
|
||||
|
||||
use fuzzy_match::FuzzyMatchResult;
|
||||
use ordered_float::OrderedFloat;
|
||||
use warp_core::ui::color::coloru_with_opacity;
|
||||
use warp_core::ui::theme::Fill;
|
||||
use warp_core::ui::Icon;
|
||||
use warpui::elements::{ConstrainedBox, Container, Highlight, ParentElement, Shrinkable, Text};
|
||||
use warpui::fonts::{Properties, Style, Weight};
|
||||
use warpui::prelude::{Align, CrossAxisAlignment, Flex, MainAxisAlignment, MainAxisSize};
|
||||
use warpui::scene::{CornerRadius, Radius};
|
||||
use warpui::text_layout::ClipConfig;
|
||||
use warpui::{AppContext, Element, SingletonEntity};
|
||||
|
||||
use crate::ai::active_agent_views_model::{ActiveAgentViewsModel, ConversationOrTaskId};
|
||||
use crate::ai::agent::conversation::ConversationStatus;
|
||||
use crate::ai::blocklist::BlocklistAIHistoryModel;
|
||||
use crate::ai::conversation_navigation::ConversationNavigationData;
|
||||
use crate::ai::conversation_status_ui::{render_status_element, STATUS_ELEMENT_PADDING};
|
||||
use crate::appearance::Appearance;
|
||||
use crate::search::{ItemHighlightState, SearchItem};
|
||||
use crate::terminal::input::conversations::AcceptConversation;
|
||||
use crate::terminal::input::inline_menu::styles as inline_styles;
|
||||
use crate::util::time_format::format_approx_duration_from_now_utc;
|
||||
|
||||
/// Search item for rendering a conversation in the inline conversation menu.
|
||||
#[derive(Debug, Clone)]
|
||||
pub(super) struct ConversationSearchItem {
|
||||
navigation_data: ConversationNavigationData,
|
||||
name_match_result: Option<FuzzyMatchResult>,
|
||||
score: OrderedFloat<f64>,
|
||||
conversation_status: Option<ConversationStatus>,
|
||||
}
|
||||
|
||||
impl ConversationSearchItem {
|
||||
pub fn new(navigation_data: ConversationNavigationData, app: &AppContext) -> Self {
|
||||
let history_model = BlocklistAIHistoryModel::as_ref(app);
|
||||
let conversation_status = history_model
|
||||
.conversation(&navigation_data.id)
|
||||
.map(|conversation| conversation.status().clone());
|
||||
|
||||
Self {
|
||||
navigation_data,
|
||||
name_match_result: None,
|
||||
score: OrderedFloat(f64::MIN),
|
||||
conversation_status,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn with_name_match_result(mut self, result: Option<FuzzyMatchResult>) -> Self {
|
||||
self.name_match_result = result;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn with_score(mut self, score: OrderedFloat<f64>) -> Self {
|
||||
self.score = score;
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl SearchItem for ConversationSearchItem {
|
||||
type Action = AcceptConversation;
|
||||
|
||||
fn render_icon(
|
||||
&self,
|
||||
_highlight_state: ItemHighlightState,
|
||||
appearance: &Appearance,
|
||||
) -> Box<dyn Element> {
|
||||
let icon_size = inline_styles::font_size(appearance);
|
||||
let icon = match &self.conversation_status {
|
||||
Some(conversation_status) => {
|
||||
render_status_element(conversation_status, icon_size, appearance)
|
||||
}
|
||||
None => {
|
||||
let icon_color = appearance
|
||||
.theme()
|
||||
.sub_text_color(appearance.theme().background());
|
||||
Container::new(
|
||||
ConstrainedBox::new(Icon::History.to_warpui_icon(icon_color).finish())
|
||||
.with_width(icon_size)
|
||||
.with_height(icon_size)
|
||||
.finish(),
|
||||
)
|
||||
.with_uniform_padding(STATUS_ELEMENT_PADDING)
|
||||
.with_background(coloru_with_opacity(icon_color.into(), 10))
|
||||
.with_corner_radius(CornerRadius::with_all(Radius::Pixels(4.)))
|
||||
.finish()
|
||||
}
|
||||
};
|
||||
|
||||
Container::new(icon)
|
||||
.with_margin_right(inline_styles::ICON_MARGIN)
|
||||
.finish()
|
||||
}
|
||||
|
||||
fn render_item(
|
||||
&self,
|
||||
_highlight_state: ItemHighlightState,
|
||||
app: &AppContext,
|
||||
) -> Box<dyn Element> {
|
||||
let appearance = Appearance::as_ref(app);
|
||||
let theme = appearance.theme();
|
||||
|
||||
let font_size = inline_styles::font_size(appearance);
|
||||
let background_color = inline_styles::menu_background_color(app);
|
||||
|
||||
let primary_text_color = inline_styles::primary_text_color(theme, background_color.into());
|
||||
let secondary_text_color = theme.disabled_text_color(background_color.into());
|
||||
|
||||
let open_conversation_ids =
|
||||
ActiveAgentViewsModel::as_ref(app).get_all_open_conversation_ids(app);
|
||||
let is_active = open_conversation_ids.contains(&ConversationOrTaskId::ConversationId(
|
||||
self.navigation_data.id,
|
||||
));
|
||||
|
||||
let secondary_suffix = " open in different pane";
|
||||
let title = &self.navigation_data.title;
|
||||
let should_show_suffix = is_active && !self.navigation_data.is_in_active_pane;
|
||||
let full_text = if should_show_suffix {
|
||||
format!("{title}{secondary_suffix}")
|
||||
} else {
|
||||
title.clone()
|
||||
};
|
||||
|
||||
let mut name_text = Text::new_inline(full_text, appearance.ui_font_family(), font_size)
|
||||
.with_color(primary_text_color.into())
|
||||
.with_clip(ClipConfig::ellipsis());
|
||||
|
||||
if let Some(name_match) = &self.name_match_result {
|
||||
if !name_match.matched_indices.is_empty() {
|
||||
name_text = name_text.with_single_highlight(
|
||||
Highlight::new().with_properties(Properties::default().weight(Weight::Bold)),
|
||||
name_match.matched_indices.clone(),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if should_show_suffix {
|
||||
let secondary_range = title.len()..(title.len() + secondary_suffix.len());
|
||||
name_text = name_text.with_single_highlight(
|
||||
Highlight::new()
|
||||
.with_properties(Properties {
|
||||
style: Style::Italic,
|
||||
..Default::default()
|
||||
})
|
||||
.with_foreground_color(secondary_text_color.into()),
|
||||
secondary_range.collect(),
|
||||
);
|
||||
}
|
||||
|
||||
let mut primary_row = Flex::row()
|
||||
.with_main_axis_size(MainAxisSize::Max)
|
||||
.with_main_axis_alignment(MainAxisAlignment::SpaceBetween)
|
||||
.with_cross_axis_alignment(CrossAxisAlignment::Center)
|
||||
.with_child(Shrinkable::new(1., name_text.finish()).finish());
|
||||
|
||||
// We want the timestamp 'column' to have fixed width so clipping is consistent,
|
||||
// limit the timestamp width to about 10 chars.
|
||||
let timestamp = Text::new_inline(
|
||||
format_approx_duration_from_now_utc(self.navigation_data.last_updated.to_utc()),
|
||||
appearance.ui_font_family(),
|
||||
font_size,
|
||||
)
|
||||
.with_color(secondary_text_color.into())
|
||||
.finish();
|
||||
|
||||
let max_timestamp_width = app
|
||||
.font_cache()
|
||||
.em_width(appearance.ui_font_family(), font_size)
|
||||
* 10.;
|
||||
primary_row.add_child(
|
||||
ConstrainedBox::new(Align::new(timestamp).right().finish())
|
||||
.with_width(max_timestamp_width)
|
||||
.finish(),
|
||||
);
|
||||
|
||||
primary_row.finish()
|
||||
}
|
||||
|
||||
fn item_background(
|
||||
&self,
|
||||
highlight_state: ItemHighlightState,
|
||||
appearance: &Appearance,
|
||||
) -> Option<Fill> {
|
||||
inline_styles::item_background(highlight_state, appearance)
|
||||
}
|
||||
|
||||
fn score(&self) -> OrderedFloat<f64> {
|
||||
self.score
|
||||
}
|
||||
|
||||
fn accept_result(&self) -> Self::Action {
|
||||
AcceptConversation {
|
||||
navigation_data: self.navigation_data.clone(),
|
||||
}
|
||||
}
|
||||
|
||||
fn execute_result(&self) -> Self::Action {
|
||||
self.accept_result()
|
||||
}
|
||||
|
||||
fn accessibility_label(&self) -> String {
|
||||
format!("Conversation: {}", self.navigation_data.title)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,210 @@
|
||||
//! Inline conversation menu view for selecting AI conversations.
|
||||
|
||||
use std::collections::HashSet;
|
||||
use std::sync::LazyLock;
|
||||
|
||||
use warpui::elements::ChildView;
|
||||
use warpui::{Element, Entity, ModelHandle, SingletonEntity, View, ViewContext, ViewHandle};
|
||||
|
||||
use crate::ai::active_agent_views_model::ActiveAgentViewsModel;
|
||||
use crate::ai::blocklist::agent_view::AgentViewController;
|
||||
use crate::ai::conversation_navigation::ConversationNavigationData;
|
||||
use crate::features::FeatureFlag;
|
||||
use crate::search::data_source::{Query, QueryFilter};
|
||||
use crate::search::mixer::SearchMixer;
|
||||
use crate::terminal::input::buffer_model::{InputBufferModel, InputBufferUpdateEvent};
|
||||
use crate::terminal::input::conversations::data_source::ConversationMenuDataSource;
|
||||
use crate::terminal::input::conversations::{AcceptConversation, InlineConversationMenuTab};
|
||||
use crate::terminal::input::inline_menu::{
|
||||
InlineMenuEvent, InlineMenuPositioner, InlineMenuTabConfig, InlineMenuView,
|
||||
};
|
||||
use crate::terminal::input::suggestions_mode_model::{
|
||||
InputSuggestionsModeEvent, InputSuggestionsModeModel,
|
||||
};
|
||||
use crate::terminal::model::session::active_session::ActiveSession;
|
||||
|
||||
/// Events emitted by InlineConversationMenuView.
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum InlineConversationMenuEvent {
|
||||
/// User 'accepted' a conversation (hit enter).
|
||||
NavigateToConversation {
|
||||
conversation_navigation_data: Box<ConversationNavigationData>,
|
||||
},
|
||||
/// User dismissed the menu (escape or click).
|
||||
Dismissed,
|
||||
}
|
||||
|
||||
static TAB_CONFIGS: LazyLock<Vec<InlineMenuTabConfig<InlineConversationMenuTab>>> =
|
||||
LazyLock::new(|| {
|
||||
let mut configs = vec![InlineMenuTabConfig {
|
||||
id: InlineConversationMenuTab::All,
|
||||
label: "All".to_string(),
|
||||
filters: HashSet::new(),
|
||||
}];
|
||||
if FeatureFlag::InlineMenuHeaders.is_enabled() {
|
||||
configs.push(InlineMenuTabConfig {
|
||||
id: InlineConversationMenuTab::CurrentDirectory,
|
||||
label: "Current Directory".to_string(),
|
||||
filters: HashSet::from([QueryFilter::CurrentDirectoryConversations]),
|
||||
});
|
||||
}
|
||||
configs
|
||||
});
|
||||
|
||||
pub struct InlineConversationMenuView {
|
||||
menu_view: ViewHandle<InlineMenuView<AcceptConversation, InlineConversationMenuTab>>,
|
||||
mixer: ModelHandle<SearchMixer<AcceptConversation>>,
|
||||
input_suggestions_model: ModelHandle<InputSuggestionsModeModel>,
|
||||
input_buffer_model: ModelHandle<InputBufferModel>,
|
||||
}
|
||||
|
||||
impl InlineConversationMenuView {
|
||||
pub fn new(
|
||||
input_suggestions_model: ModelHandle<InputSuggestionsModeModel>,
|
||||
agent_view_controller: ModelHandle<AgentViewController>,
|
||||
input_buffer_model: &ModelHandle<InputBufferModel>,
|
||||
positioner: &ModelHandle<InlineMenuPositioner>,
|
||||
active_session: ModelHandle<ActiveSession>,
|
||||
ctx: &mut ViewContext<Self>,
|
||||
) -> Self {
|
||||
let data_source = ctx.add_model(|_| {
|
||||
ConversationMenuDataSource::new(agent_view_controller.clone(), active_session)
|
||||
});
|
||||
|
||||
let tab_configs = TAB_CONFIGS.clone();
|
||||
let initial_filters = tab_configs
|
||||
.first()
|
||||
.map(|config| config.filters.clone())
|
||||
.unwrap_or_default();
|
||||
|
||||
let mixer = ctx.add_model(|ctx| {
|
||||
let mut mixer = SearchMixer::<AcceptConversation>::new();
|
||||
mixer.add_sync_source(data_source, [QueryFilter::CurrentDirectoryConversations]);
|
||||
mixer.run_query(
|
||||
Query {
|
||||
text: String::new(),
|
||||
filters: initial_filters,
|
||||
},
|
||||
ctx,
|
||||
);
|
||||
mixer
|
||||
});
|
||||
|
||||
let menu_view = ctx.add_typed_action_view(|ctx| {
|
||||
InlineMenuView::new_with_tabs(
|
||||
mixer.clone(),
|
||||
positioner.clone(),
|
||||
&input_suggestions_model,
|
||||
agent_view_controller,
|
||||
tab_configs,
|
||||
None,
|
||||
ctx,
|
||||
)
|
||||
});
|
||||
ctx.subscribe_to_view(&menu_view, |me, _, event, ctx| match event {
|
||||
InlineMenuEvent::AcceptedItem { item, .. } => {
|
||||
ctx.emit(InlineConversationMenuEvent::NavigateToConversation {
|
||||
conversation_navigation_data: Box::new(item.navigation_data.clone()),
|
||||
});
|
||||
}
|
||||
InlineMenuEvent::SelectedItem { .. } | InlineMenuEvent::NoResults => (),
|
||||
InlineMenuEvent::TabChanged => {
|
||||
me.rerun_query(ctx);
|
||||
}
|
||||
InlineMenuEvent::Dismissed => {
|
||||
ctx.emit(InlineConversationMenuEvent::Dismissed);
|
||||
}
|
||||
});
|
||||
|
||||
ctx.subscribe_to_model(
|
||||
&input_suggestions_model,
|
||||
|me, input_suggestions_model, event, ctx| {
|
||||
let InputSuggestionsModeEvent::ModeChanged { .. } = event;
|
||||
if input_suggestions_model.as_ref(ctx).is_conversation_menu() {
|
||||
me.rerun_query(ctx);
|
||||
}
|
||||
},
|
||||
);
|
||||
ctx.subscribe_to_model(input_buffer_model, |me, _, event, ctx| {
|
||||
if me
|
||||
.input_suggestions_model
|
||||
.as_ref(ctx)
|
||||
.is_conversation_menu()
|
||||
{
|
||||
let InputBufferUpdateEvent { new_content: _, .. } = event;
|
||||
me.rerun_query(ctx);
|
||||
}
|
||||
});
|
||||
|
||||
let active_agent_views_model = ActiveAgentViewsModel::handle(ctx);
|
||||
ctx.subscribe_to_model(&active_agent_views_model, |me, _, _, ctx| {
|
||||
if me
|
||||
.input_suggestions_model
|
||||
.as_ref(ctx)
|
||||
.is_conversation_menu()
|
||||
{
|
||||
me.menu_view.update(ctx, |_, ctx| ctx.notify());
|
||||
}
|
||||
});
|
||||
|
||||
Self {
|
||||
menu_view,
|
||||
mixer,
|
||||
input_suggestions_model,
|
||||
input_buffer_model: input_buffer_model.clone(),
|
||||
}
|
||||
}
|
||||
|
||||
fn rerun_query(&self, ctx: &mut ViewContext<Self>) {
|
||||
let filters = self
|
||||
.menu_view
|
||||
.as_ref(ctx)
|
||||
.model()
|
||||
.as_ref(ctx)
|
||||
.active_tab_filters();
|
||||
self.mixer.update(ctx, |mixer, ctx| {
|
||||
mixer.run_query(
|
||||
Query {
|
||||
text: self
|
||||
.input_buffer_model
|
||||
.as_ref(ctx)
|
||||
.current_value()
|
||||
.to_owned(),
|
||||
filters,
|
||||
},
|
||||
ctx,
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
pub fn select_next_tab(&self, ctx: &mut ViewContext<Self>) -> bool {
|
||||
self.menu_view.update(ctx, |v, ctx| v.select_next_tab(ctx))
|
||||
}
|
||||
|
||||
pub fn select_up(&self, ctx: &mut ViewContext<Self>) {
|
||||
self.menu_view.update(ctx, |v, ctx| v.select_up(ctx));
|
||||
}
|
||||
|
||||
pub fn select_down(&self, ctx: &mut ViewContext<Self>) {
|
||||
self.menu_view.update(ctx, |v, ctx| v.select_down(ctx));
|
||||
}
|
||||
|
||||
pub fn accept_selected_item(&self, ctx: &mut ViewContext<Self>) {
|
||||
self.menu_view
|
||||
.update(ctx, |v, ctx| v.accept_selected_item(false, ctx));
|
||||
}
|
||||
}
|
||||
|
||||
impl View for InlineConversationMenuView {
|
||||
fn ui_name() -> &'static str {
|
||||
"InlineConversationMenuView"
|
||||
}
|
||||
|
||||
fn render(&self, _app: &warpui::AppContext) -> Box<dyn Element> {
|
||||
ChildView::new(&self.menu_view).finish()
|
||||
}
|
||||
}
|
||||
|
||||
impl Entity for InlineConversationMenuView {
|
||||
type Event = InlineConversationMenuEvent;
|
||||
}
|
||||
Reference in New Issue
Block a user