first pass of merging in warp (doesn't build)

This commit is contained in:
Ryan Ward
2026-07-01 16:08:58 -05:00
parent 2f64909469
commit 4770ac06b5
3662 changed files with 414574 additions and 89772 deletions
+192 -186
View File
@@ -1,89 +1,75 @@
use std::{collections::HashSet, sync::Arc, time::Duration};
use std::collections::HashSet;
use std::sync::Arc;
use std::time::Duration;
use super::{
cli_controller::{CLISubagentController, CLISubagentEvent, UserTakeOverReason},
model::{AIBlockModel, AIBlockModelImpl, AIBlockOutputStatus},
view_impl::common::{
random_load_output_message, render_switch_control_to_user_button, render_warping_indicator,
render_warping_indicator_base, ButtonProps, ForceRefreshButtonProps, MaybeShimmeringText,
WarpingIndicatorProps, WarpingProps, WAITING_FOR_USER_INPUT_MESSAGE,
},
};
use crate::{
ai::agent_tips::AITipModel,
terminal::{
input::buffer_model::InputBufferUpdateEvent,
view::ambient_agent::is_cloud_agent_pre_first_exchange,
},
};
use crate::{
ai::blocklist::agent_view::{
agent_view_bg_fill, child_agent_status_card::ChildAgentStatusCard, AgentMessageBar,
AgentViewController, EphemeralMessageModel,
},
terminal::input::{
buffer_model::InputBufferModel,
message_bar::common::render_standard_message_bar,
message_bar::{Message, MessageItem},
slash_command_model::SlashCommandModel,
suggestions_mode_model::InputSuggestionsModeModel,
},
};
use warp_multi_agent_api as api;
use crate::{
ai::{
agent::{
conversation::AIConversationId, icons, AIAgentExchangeId, AIAgentOutput,
AIAgentOutputMessageType, CancellationReason, SummarizationType,
},
blocklist::{
agent_view::shortcuts::AgentShortcutViewModel,
ai_brand_color,
model::AIBlockModelHelper,
summarization_cancel_dialog::{
self, SummarizationCancelDialog, SummarizationCancelDialogEvent,
},
BlocklistAIActionEvent, BlocklistAIActionModel, BlocklistAIContextEvent,
BlocklistAIContextModel, BlocklistAIController, BlocklistAIHistoryEvent,
BlocklistAIInputEvent, BlocklistAIInputModel, ResponseStreamId,
},
llms::LLMPreferences,
AgentTip,
},
send_telemetry_from_app_ctx,
server::telemetry::TelemetryEvent,
settings::{InputModeSettings, InputSettings},
settings_view::keybindings::KeybindingChangedNotifier,
terminal::{
input::SET_INPUT_MODE_TERMINAL_ACTION_NAME,
model::block::LONG_RUNNING_COMMAND_DURATION_MS,
model_events::{ModelEvent, ModelEventDispatcher},
view::ambient_agent::{AmbientAgentViewModel, AmbientAgentViewModelEvent},
warpify::render::LEFT_STRIPE_WIDTH,
TerminalModel, CANCEL_COMMAND_KEYBINDING, TOGGLE_AUTOEXECUTE_MODE_KEYBINDING,
TOGGLE_HIDE_CLI_RESPONSES_KEYBINDING, TOGGLE_QUEUE_NEXT_PROMPT_KEYBINDING,
},
util::bindings::keybinding_name_to_keystroke,
BlocklistAIHistoryModel,
};
use galaxy_core::{
features::FeatureFlag,
ui::{appearance::Appearance, theme::Fill, Icon as CoreIcon},
};
use galaxyui::elements::shimmering_text::ShimmeringTextStateHandle;
use galaxyui::{
elements::{Border, Container, Empty, Flex, MouseStateHandle, ParentElement, Text},
keymap::Keystroke,
presenter::ChildView,
r#async::SpawnedFutureHandle,
AppContext, Element, Entity, EntityId, ModelHandle, SingletonEntity, View, ViewContext,
ViewHandle,
};
use galaxyui::{r#async::Timer, TypedActionView};
use instant::Instant;
use markdown_parser::FormattedTextFragment;
use parking_lot::FairMutex;
use pathfinder_color::ColorU;
use galaxy_core::features::FeatureFlag;
use galaxy_core::ui::appearance::Appearance;
use galaxy_core::ui::theme::Fill;
use galaxy_core::ui::Icon as CoreIcon;
use warp_multi_agent_api as api;
use galaxyui::elements::shimmering_text::ShimmeringTextStateHandle;
use galaxyui::elements::{Border, Container, Empty, Flex, MouseStateHandle, ParentElement, Text};
use galaxyui::keymap::Keystroke;
use galaxyui::presenter::ChildView;
use galaxyui::r#async::{SpawnedFutureHandle, Timer};
use galaxyui::{
AppContext, Element, Entity, EntityId, ModelHandle, SingletonEntity, TypedActionView, View,
ViewContext, ViewHandle,
};
use super::cli_controller::{CLISubagentController, CLISubagentEvent, UserTakeOverReason};
use super::model::{AIBlockModel, AIBlockModelImpl, AIBlockOutputStatus};
use super::view_impl::common::{
render_switch_control_to_user_button, render_warping_indicator, render_warping_indicator_base,
AutoExecuteButtonProps, ButtonProps, ForceRefreshButtonProps, MaybeShimmeringText,
WarpingIndicatorProps, WarpingProps, LOAD_OUTPUT_MESSAGE, WAITING_FOR_USER_INPUT_MESSAGE,
};
use crate::ai::agent::conversation::AIConversationId;
use crate::ai::agent::{
icons, AIAgentExchangeId, AIAgentOutput, AIAgentOutputMessageType, CancellationReason,
SummarizationType,
};
use crate::ai::agent_tips::AITipModel;
use crate::ai::blocklist::agent_view::shortcuts::AgentShortcutViewModel;
use crate::ai::blocklist::agent_view::{
agent_view_bg_fill, is_in_cloud_context, AgentMessageBar, AgentViewController,
EphemeralMessageModel,
};
use crate::ai::blocklist::model::AIBlockModelHelper;
use crate::ai::blocklist::summarization_cancel_dialog::{
self, SummarizationCancelDialog, SummarizationCancelDialogEvent,
};
use crate::ai::blocklist::{
ai_brand_color, BlocklistAIActionEvent, BlocklistAIActionModel, BlocklistAIContextEvent,
BlocklistAIContextModel, BlocklistAIController, BlocklistAIHistoryEvent, BlocklistAIInputEvent,
BlocklistAIInputModel, QueuedQueryEvent, QueuedQueryModel, ResponseStreamId,
};
use crate::ai::llms::LLMPreferences;
use crate::ai::AgentTip;
use crate::server::telemetry::TelemetryEvent;
use crate::settings::{InputModeSettings, InputSettings};
use crate::settings_view::keybindings::KeybindingChangedNotifier;
use crate::terminal::input::buffer_model::{InputBufferModel, InputBufferUpdateEvent};
use crate::terminal::input::message_bar::common::render_wrapping_standard_message_bar;
use crate::terminal::input::slash_command_model::SlashCommandModel;
use crate::terminal::input::suggestions_mode_model::InputSuggestionsModeModel;
use crate::terminal::input::{HandoffComposeState, SET_INPUT_MODE_TERMINAL_ACTION_NAME};
use crate::terminal::model::block::LONG_RUNNING_COMMAND_DURATION_MS;
use crate::terminal::model_events::{ModelEvent, ModelEventDispatcher};
use crate::terminal::view::ambient_agent::{
is_cloud_agent_pre_first_exchange, AmbientAgentViewModel, AmbientAgentViewModelEvent,
};
use crate::terminal::warpify::render::LEFT_STRIPE_WIDTH;
use crate::terminal::{
TerminalModel, CANCEL_COMMAND_KEYBINDING, TOGGLE_AUTOEXECUTE_MODE_KEYBINDING,
TOGGLE_HIDE_CLI_RESPONSES_KEYBINDING, TOGGLE_QUEUE_NEXT_PROMPT_KEYBINDING,
};
use crate::util::bindings::keybinding_name_to_keystroke;
use crate::{send_telemetry_from_app_ctx, BlocklistAIHistoryModel};
pub fn init(app: &mut AppContext) {
summarization_cancel_dialog::init(app);
@@ -96,7 +82,6 @@ struct StateHandles {
stop_button: MouseStateHandle,
take_over_button: MouseStateHandle,
hide_cli_responses_button: MouseStateHandle,
github_auth_link: MouseStateHandle,
/// Tracks hover/press state for the inline `Check now` affordance rendered next to
/// `Last seen by agent ...` while the agent is polling a long-running command.
force_refresh_button: MouseStateHandle,
@@ -113,7 +98,7 @@ pub struct BlocklistAIStatusBar {
terminal_model: Arc<FairMutex<TerminalModel>>,
shimmering_text_handle: ShimmeringTextStateHandle,
state_handles: StateHandles,
ambient_agent_view_model: ModelHandle<AmbientAgentViewModel>,
ambient_agent_view_model: Option<ModelHandle<AmbientAgentViewModel>>,
autoexecute_keystroke: Option<Keystroke>,
queue_next_prompt_keystroke: Option<Keystroke>,
@@ -146,7 +131,6 @@ pub struct BlocklistAIStatusBar {
ephemeral_message_model: ModelHandle<EphemeralMessageModel>,
agent_message_bar: ViewHandle<AgentMessageBar>,
child_agent_status_card: ViewHandle<ChildAgentStatusCard>,
}
impl BlocklistAIStatusBar {
@@ -162,17 +146,18 @@ impl BlocklistAIStatusBar {
model_event_dispatcher: &ModelHandle<ModelEventDispatcher>,
terminal_model: Arc<FairMutex<TerminalModel>>,
shortcut_view_model: ModelHandle<AgentShortcutViewModel>,
ambient_agent_view_model: ModelHandle<AmbientAgentViewModel>,
ambient_agent_view_model: Option<ModelHandle<AmbientAgentViewModel>>,
input_suggestions_model: ModelHandle<InputSuggestionsModeModel>,
slash_command_model: ModelHandle<SlashCommandModel>,
ephemeral_message_model: ModelHandle<EphemeralMessageModel>,
handoff_compose_state: ModelHandle<HandoffComposeState>,
terminal_view_id: EntityId,
ctx: &mut ViewContext<Self>,
) -> Self {
let history_model = BlocklistAIHistoryModel::handle(ctx);
ctx.subscribe_to_model(&history_model, move |me, _, event, ctx| {
if event
.terminal_view_id()
.terminal_surface_id()
.is_some_and(|id| id != terminal_view_id)
{
return;
@@ -189,7 +174,7 @@ impl BlocklistAIStatusBar {
}
me.reset_model_for_exchange(*exchange_id, *conversation_id, ctx);
}
BlocklistAIHistoryEvent::ClearedConversationsInTerminalView { .. } => {
BlocklistAIHistoryEvent::ClearedConversationsForTerminalSurface { .. } => {
me.active_exchange_model = None;
me.stop_warping_timer();
ctx.notify();
@@ -234,10 +219,15 @@ impl BlocklistAIStatusBar {
}
});
ctx.subscribe_to_model(&context_model, |_, _, event, ctx| {
if matches!(event, BlocklistAIContextEvent::PendingQueryStateUpdated) {
ctx.notify();
}
});
ctx.subscribe_to_model(&QueuedQueryModel::handle(ctx), |_, _, event, ctx| {
if matches!(
event,
BlocklistAIContextEvent::PendingQueryStateUpdated
| BlocklistAIContextEvent::QueueNextPromptToggled
QueuedQueryEvent::QueueNextPromptToggled { .. }
| QueuedQueryEvent::DefaultModeChanged
) {
ctx.notify();
}
@@ -303,6 +293,11 @@ impl BlocklistAIStatusBar {
ctx.notify();
});
ctx.observe(&AITipModel::handle(ctx), |me, tip_model, ctx| {
me.current_tip = tip_model.as_ref(ctx).current_tip().cloned();
ctx.notify();
});
let summarization_cancel_dialog =
ctx.add_typed_action_view(|_| SummarizationCancelDialog::default());
ctx.subscribe_to_view(
@@ -347,7 +342,7 @@ impl BlocklistAIStatusBar {
ctx.notify();
});
let agent_message_bar = ctx.add_view(|ctx| {
let agent_message_bar = ctx.add_typed_action_view(|ctx| {
AgentMessageBar::new(
agent_view_controller.clone(),
ephemeral_message_model.clone(),
@@ -357,28 +352,28 @@ impl BlocklistAIStatusBar {
input_suggestions_model,
slash_command_model,
context_model.clone(),
handoff_compose_state,
terminal_model.clone(),
ctx,
)
});
let child_agent_status_card = ctx.add_typed_action_view(|ctx| {
ChildAgentStatusCard::new(agent_view_controller.clone(), ctx)
});
ctx.subscribe_to_model(&ambient_agent_view_model, |me, _, event, ctx| match event {
AmbientAgentViewModelEvent::DispatchedAgent
| AmbientAgentViewModelEvent::ProgressUpdated => {
me.update_agent_tip(ctx);
ctx.notify();
}
AmbientAgentViewModelEvent::SessionReady { .. }
| AmbientAgentViewModelEvent::Failed { .. }
| AmbientAgentViewModelEvent::NeedsGithubAuth
| AmbientAgentViewModelEvent::Cancelled => {
ctx.notify();
}
_ => (),
});
if let Some(ambient_agent_view_model) = ambient_agent_view_model.as_ref() {
ctx.subscribe_to_model(ambient_agent_view_model, |me, _, event, ctx| match event {
AmbientAgentViewModelEvent::DispatchedAgent
| AmbientAgentViewModelEvent::ProgressUpdated => {
me.update_agent_tip(ctx);
ctx.notify();
}
AmbientAgentViewModelEvent::SessionReady { .. }
| AmbientAgentViewModelEvent::Failed { .. }
| AmbientAgentViewModelEvent::NeedsGithubAuth
| AmbientAgentViewModelEvent::Cancelled => {
ctx.notify();
}
_ => (),
});
}
Self {
active_exchange_model: None,
@@ -409,7 +404,6 @@ impl BlocklistAIStatusBar {
current_tip: None,
ephemeral_message_model,
agent_message_bar,
child_agent_status_card,
}
}
@@ -909,23 +903,28 @@ impl BlocklistAIStatusBar {
shimmering_text_handle: &self.shimmering_text_handle,
summarization_start_time: self.summarization_start_time,
auto_execute_button: (!model.request_type(app).is_passive_code_diff()).then_some(
ButtonProps {
AutoExecuteButtonProps {
button_handle: &self.state_handles.autoexecute_button,
keystroke: self.autoexecute_keystroke.as_ref(),
is_active: model
.conversation(app)
.map(|c| c.autoexecute_any_action())
.unwrap_or(false),
is_locked: is_in_cloud_context(
terminal_model.block_list().agent_view_state(),
&terminal_model,
),
},
),
queue_next_prompt_button: FeatureFlag::QueueSlashCommand.is_enabled().then_some(
ButtonProps {
button_handle: &self.state_handles.queue_next_prompt_button,
keystroke: self.queue_next_prompt_keystroke.as_ref(),
is_active: self
.context_model
.as_ref(app)
.is_queue_next_prompt_enabled(),
is_active: QueuedQueryModel::as_ref(app).is_queue_next_prompt_enabled(
conversation.id(),
active_block,
app,
),
},
),
stop_button: Some(ButtonProps {
@@ -961,16 +960,21 @@ impl BlocklistAIStatusBar {
return None;
}
let ambient_agent_model = self.ambient_agent_view_model.as_ref(app);
let ambient_agent_model = self
.ambient_agent_view_model
.as_ref()
.map(|ambient_agent_view_model| ambient_agent_view_model.as_ref(app))?;
// The step indicator is only meaningful while a spawn is in flight. Terminal states
// (`Failed`, `NeedsGithubAuth`, `Cancelled`) still carry an `AgentProgress` for
// telemetry purposes, so guard on `is_waiting_for_session()` rather than relying on
// `agent_progress()` being `None`.
if !ambient_agent_model.is_waiting_for_session() {
return None;
}
let progress = ambient_agent_model.agent_progress()?;
let progress_text = if progress.harness_started_at.is_some() {
"Starting Environment (Step 3/3)"
} else if progress.claimed_at.is_some() {
"Creating Environment (Step 2/3)"
} else {
"Connecting to Host (Step 1/3)"
};
let progress_text = progress.setup_status_text();
Some(render_warping_indicator_base(
WarpingIndicatorProps {
icon: None,
@@ -988,58 +992,58 @@ impl BlocklistAIStatusBar {
))
}
fn render_cloud_mode_setup_terminal_message(&self, app: &AppContext) -> Option<Message> {
fn render_cloud_mode_setup_terminal_message(
&self,
app: &AppContext,
) -> Option<Box<dyn Element>> {
if !FeatureFlag::CloudModeSetupV2.is_enabled() {
return None;
}
let ambient_agent_model = self.ambient_agent_view_model.as_ref(app);
let ambient_agent_model = self
.ambient_agent_view_model
.as_ref()
.map(|ambient_agent_view_model| ambient_agent_view_model.as_ref(app))?;
let theme = Appearance::as_ref(app).theme();
let error_color = theme.ansi_fg_red();
if let Some(auth_url) = ambient_agent_model.github_auth_url() {
return Some(Message::new(vec![
MessageItem::Icon {
icon: CoreIcon::Triangle,
color: Some(error_color),
},
MessageItem::Text {
content: "Missing GitHub authentication. ".into(),
color: Some(error_color),
},
MessageItem::hyperlink(
"Authenticate GitHub",
auth_url.to_owned(),
self.state_handles.github_auth_link.clone(),
),
]));
}
if let Some(error_message) = ambient_agent_model.error_message() {
return Some(Message::new(vec![
MessageItem::Icon {
icon: CoreIcon::Triangle,
color: Some(error_color),
},
MessageItem::Text {
content: error_message.to_owned().into(),
color: Some(error_color),
},
]));
let error_message = ambient_agent_model
.github_auth_error_message()
.unwrap_or("Missing GitHub authentication.");
return Some(render_wrapping_standard_message_bar(
CoreIcon::Triangle,
error_color,
error_color,
vec![
FormattedTextFragment::plain_text(format!("{error_message} ")),
FormattedTextFragment::hyperlink("Authenticate GitHub", auth_url.to_owned()),
],
app,
));
}
if ambient_agent_model.is_cancelled() {
let color = theme.disabled_text_color(theme.background()).into_solid();
return Some(Message::new(vec![
MessageItem::Icon {
icon: CoreIcon::StopFilled,
color: Some(color),
},
MessageItem::Text {
content: "Cloud agent run cancelled".into(),
color: Some(color),
},
]));
return Some(render_wrapping_standard_message_bar(
CoreIcon::StopFilled,
color,
color,
vec![FormattedTextFragment::plain_text(
"Cloud agent run cancelled",
)],
app,
));
}
if let Some(error_message) = ambient_agent_model.error_message() {
return Some(render_wrapping_standard_message_bar(
CoreIcon::Triangle,
error_color,
error_color,
vec![FormattedTextFragment::plain_text(error_message.to_owned())],
app,
));
}
None
@@ -1065,15 +1069,16 @@ fn latest_model_used_before_exchange<V: View>(
model_id: model_info.model_id.to_string(),
model_display_name: model_info.display_name.clone(),
is_fallback: model_info.is_fallback,
prompt_cache_expires_at: None,
})
})
}
fn render_agent_tip(tip: &AgentTip, app: &AppContext) -> Box<dyn Element> {
use crate::ai::agent_tips::AITip;
use galaxyui::text_layout::ClipConfig;
use markdown_parser::{FormattedTextFragment, FormattedTextLine};
use crate::ai::agent_tips::AITip;
let appearance = Appearance::as_ref(app);
let theme = appearance.theme();
@@ -1218,17 +1223,24 @@ impl View for BlocklistAIStatusBar {
if let Some(cloud_mode_setup_terminal_message) =
self.render_cloud_mode_setup_terminal_message(app)
{
return render_standard_message_bar(cloud_mode_setup_terminal_message, None, app);
return cloud_mode_setup_terminal_message;
}
let status_element =
if let Some(cloud_mode_setup_status) = self.render_cloud_mode_setup_status(app) {
cloud_mode_setup_status
} else if FeatureFlag::CloudModeSetupV2.is_enabled()
&& is_cloud_agent_pre_first_exchange(
&self.ambient_agent_view_model,
&self.agent_view_controller,
app,
)
&& self
.ambient_agent_view_model
.as_ref()
.is_some_and(|ambient_agent_view_model| {
let terminal_model = self.terminal_model.lock();
is_cloud_agent_pre_first_exchange(
Some(ambient_agent_view_model),
&self.agent_view_controller,
&terminal_model,
app,
)
})
{
render_warping_indicator_base(
WarpingIndicatorProps {
@@ -1288,16 +1300,20 @@ impl View for BlocklistAIStatusBar {
.is_none(),
) {
warping_indicator
} else if self
.ambient_agent_view_model
.as_ref(app)
.is_waiting_for_session()
{
} else if self.ambient_agent_view_model.as_ref().is_some_and(
|ambient_agent_view_model| {
ambient_agent_view_model
.as_ref(app)
.is_waiting_for_session()
},
) {
// Don't render warping indicator - the loading screen is shown in the main view
return Empty::new().finish();
} else if agent_view_controller.is_active() {
// The orchestration pill bar in the agent view header
// replaces the legacy child-agent status card rows;
// render only the message bar here.
return Flex::column()
.with_child(ChildView::new(&self.child_agent_status_card).finish())
.with_child(ChildView::new(&self.agent_message_bar).finish())
.finish();
} else {
@@ -1363,16 +1379,6 @@ impl View for BlocklistAIStatusBar {
container = container.with_vertical_padding(8.);
}
// When the agent view is active, keep the child agent status card
// visible above the warping/status indicator so it doesn't disappear
// while the agent is working.
if agent_view_controller.is_active() {
return Flex::column()
.with_child(ChildView::new(&self.child_agent_status_card).finish())
.with_child(container.finish())
.finish();
}
container.finish()
}
}