Files
galaxy/app/src/terminal/view/ambient_agent/view_impl.rs
T

835 lines
34 KiB
Rust

//! [`TerminalView`]-specific implementation for ambient agent functionality.
use std::cell::Cell;
use std::rc::Rc;
use warp_cli::agent::Harness;
use warp_terminal::model::BlockId;
use crate::ai::agent::conversation::{AIConversationId, ConversationStatus};
use crate::ai::AIRequestUsageModel;
use warp_core::features::FeatureFlag;
use warp_core::send_telemetry_from_ctx;
use warpui::prelude::{Empty, Vector2F};
use crate::ai::ambient_agents::telemetry::{CloudAgentTelemetryEvent, CloudModeEntryPoint};
use crate::ai::blocklist::{agent_view::AgentViewEntryOrigin, BlocklistAIHistoryModel};
use crate::ai::conversation_details_panel::ConversationDetailsData;
use crate::pane_group::TerminalViewResources;
use crate::server::server_api::ai::SpawnAgentRequest;
use crate::terminal::view::ambient_agent::CloudModeInitialUserQuery;
use crate::terminal::view::rich_content::{RichContentInsertionPosition, RichContentMetadata};
use crate::terminal::view::TerminalView;
use crate::terminal::CLIAgent;
use crate::workspace::view::cloud_agent_capacity_modal::CloudAgentCapacityModalVariant;
use crate::workspaces::user_workspaces::UserWorkspaces;
use warp_core::ui::appearance::Appearance;
use warpui::elements::Align;
use warpui::{AppContext, Element, EntityId, SingletonEntity, ViewContext};
use super::loading_screen::{
render_cloud_mode_cancelled_screen, render_cloud_mode_error_screen,
render_cloud_mode_github_auth_required_screen, render_cloud_mode_loading_screen,
};
use super::{
is_cloud_agent_pre_first_exchange, AmbientAgentEntryBlock, AmbientAgentViewModelEvent,
};
const CHILD_AGENT_GITHUB_AUTH_REQUIRED_BLOCKED_ACTION: &str =
"GitHub authentication required before starting the child agent.";
impl TerminalView {
fn active_ambient_agent_conversation_id(&self, ctx: &AppContext) -> Option<AIConversationId> {
self.agent_view_controller
.as_ref(ctx)
.agent_view_state()
.active_conversation_id()
}
fn active_ambient_agent_conversation_is_child(&self, ctx: &AppContext) -> bool {
let Some(conversation_id) = self.active_ambient_agent_conversation_id(ctx) else {
return false;
};
BlocklistAIHistoryModel::as_ref(ctx)
.conversation(&conversation_id)
.is_some_and(|conversation| conversation.is_child_agent_conversation())
}
fn update_active_ambient_agent_conversation_status(
&self,
status: ConversationStatus,
error_message: Option<String>,
ctx: &mut ViewContext<Self>,
) {
let Some(conversation_id) = self.active_ambient_agent_conversation_id(ctx) else {
return;
};
BlocklistAIHistoryModel::handle(ctx).update(ctx, |history_model, ctx| {
history_model.update_conversation_status_with_error_message(
self.id(),
conversation_id,
status,
error_message,
ctx,
);
});
}
pub(in crate::terminal::view) fn show_out_of_credits_modal(&self, ctx: &mut ViewContext<Self>) {
let is_on_paid_plan = UserWorkspaces::as_ref(ctx)
.current_workspace()
.is_some_and(|workspace| workspace.billing_metadata.is_user_on_paid_plan());
if is_on_paid_plan {
ctx.emit(crate::terminal::view::Event::ShowCloudAgentCapacityModal {
variant: CloudAgentCapacityModalVariant::OutOfCredits,
});
} else {
AIRequestUsageModel::handle(ctx).update(ctx, |model, ctx| {
model.refresh_request_usage_async(ctx);
});
}
}
/// Handles ambient agent view model events.
pub(in crate::terminal::view) fn handle_ambient_agent_event(
&mut self,
event: &AmbientAgentViewModelEvent,
ctx: &mut ViewContext<Self>,
) {
// Tear down the non-oz cloud-mode queued-prompt block on terminal / transition
// events that replace it. `Failed`, `NeedsGithubAuth`, and `Cancelled` hand off
// to the existing error / auth / cancelled UI; `HarnessCommandStarted` hands
// off to the live harness CLI block. Idempotent and cheap when no block exists.
if matches!(
event,
AmbientAgentViewModelEvent::Failed { .. }
| AmbientAgentViewModelEvent::NeedsGithubAuth
| AmbientAgentViewModelEvent::Cancelled
| AmbientAgentViewModelEvent::HarnessCommandStarted
) {
self.remove_pending_user_query_block(ctx);
}
match event {
AmbientAgentViewModelEvent::EnteredSetupState => {
// Re-render to show the setup view.
self.update_pane_configuration(ctx);
ctx.notify();
}
AmbientAgentViewModelEvent::EnteredComposingState => {
// Update pane configuration to show cloud indicator.
self.update_pane_configuration(ctx);
}
AmbientAgentViewModelEvent::DispatchedAgent => {
// Pane chrome (e.g. cloud indicator, task id) must update on viewer surfaces
// too, so this runs above the viewer short-circuit below.
self.update_pane_configuration(ctx);
// Only the spawner's view handles `DispatchedAgent`. Viewer surfaces (shared
// ambient agent session or transcript viewer) have no submitted prompt to render
// and should not insert cloud-mode rich content here.
let is_viewer = self.is_shared_ambient_agent_session()
|| self.model.lock().is_conversation_transcript_viewer();
if is_viewer {
ctx.notify();
return;
}
if FeatureFlag::CloudModeSetupV2.is_enabled() {
if self
.ambient_agent_view_model
.as_ref(ctx)
.is_third_party_harness()
{
// Non-oz runs: render the submitted prompt via the queued-prompt UI.
// The block is removed later by `HarnessCommandStarted` / failure /
// cancel / auth handlers.
let prompt = self
.ambient_agent_view_model
.as_ref(ctx)
.request()
.map(|request| request.prompt.clone())
.unwrap_or_default();
if !prompt.is_empty() {
self.insert_cloud_mode_queued_user_query_block(prompt, ctx);
}
} else {
let initial_user_query = ctx.add_view(|ctx| {
CloudModeInitialUserQuery::new(
self.ambient_agent_view_model.clone(),
ctx,
)
});
self.insert_rich_content(
None,
initial_user_query,
None,
RichContentInsertionPosition::Append {
insert_below_long_running_block: true,
},
ctx,
);
self.ambient_agent_view_model.update(ctx, |model, _| {
model.set_has_inserted_cloud_mode_user_query_block(true);
});
}
} else {
// Reset tip cooldown so the first tip shows for 60 seconds
let tip_model = self
.ambient_agent_view_model
.as_ref(ctx)
.ui_state
.tip_model
.clone();
tip_model.update(ctx, |model, model_ctx| {
model.reset_cooldown(model_ctx);
});
}
// Re-render to show loading state.
ctx.notify();
}
AmbientAgentViewModelEvent::SessionReady { .. } => {
// Auto-open details panel for local cloud mode once the session is ready.
self.maybe_auto_open_cloud_mode_details_panel(ctx);
// Re-render to hide the loading screen now that the session is ready.
ctx.notify();
}
AmbientAgentViewModelEvent::EnvironmentSelected => {}
AmbientAgentViewModelEvent::ProgressUpdated => {
// Refresh the tip (respects 60s cooldown internally)
let tip_model = self
.ambient_agent_view_model
.as_ref(ctx)
.ui_state
.tip_model
.clone();
tip_model.update(ctx, |model, model_ctx| {
model.maybe_refresh_tip(model_ctx);
});
// Update pane header to reflect any changes (e.g., task_id being set)
self.update_pane_configuration(ctx);
ctx.notify();
}
AmbientAgentViewModelEvent::Failed { error_message } => {
self.update_active_ambient_agent_conversation_status(
ConversationStatus::Error,
Some(error_message.clone()),
ctx,
);
// Refresh the details panel to show failed status
if self.is_cloud_mode_details_panel_open {
self.fetch_and_update_cloud_mode_details_panel(ctx);
}
// Re-render to show the error state in the footer.
ctx.notify();
}
AmbientAgentViewModelEvent::ShowCloudAgentCapacityModal => {
if FeatureFlag::CloudMode.is_enabled()
&& self.ambient_agent_view_model.as_ref(ctx).is_ambient_agent()
&& !self.model.lock().is_shared_ambient_agent_session()
{
ctx.emit(crate::terminal::view::Event::ShowCloudAgentCapacityModal {
variant: CloudAgentCapacityModalVariant::ConcurrentLimit,
});
}
ctx.notify();
}
AmbientAgentViewModelEvent::ShowAICreditModal => {
if FeatureFlag::CloudMode.is_enabled()
&& self.ambient_agent_view_model.as_ref(ctx).is_ambient_agent()
&& !self.model.lock().is_shared_ambient_agent_session()
{
self.show_out_of_credits_modal(ctx);
}
ctx.notify();
}
AmbientAgentViewModelEvent::NeedsGithubAuth => {
if self.active_ambient_agent_conversation_is_child(ctx) {
self.update_active_ambient_agent_conversation_status(
ConversationStatus::Blocked {
blocked_action: CHILD_AGENT_GITHUB_AUTH_REQUIRED_BLOCKED_ACTION
.to_string(),
},
None,
ctx,
);
}
// Re-render to show the GitHub auth required state in the footer.
ctx.notify();
}
AmbientAgentViewModelEvent::Cancelled => {
self.update_active_ambient_agent_conversation_status(
ConversationStatus::Cancelled,
None,
ctx,
);
// Refresh the details panel to show cancelled status
if self.is_cloud_mode_details_panel_open {
self.fetch_and_update_cloud_mode_details_panel(ctx);
}
// Re-render to show the cancelled state in the footer.
ctx.notify();
}
AmbientAgentViewModelEvent::HarnessSelected => {
self.maybe_enter_agent_view_for_shared_third_party_viewer(ctx);
ctx.notify();
}
AmbientAgentViewModelEvent::HarnessCommandStarted => {
// Stop classifying new blocks as environment setup commands, mirroring the
// Oz path in the `AppendedExchange` handler. Flipping this flag to `false`
// also un-hides and un-marks the active block so it renders like a normal
// CLI-agent session.
{
let mut model = self.model.lock();
if model
.block_list()
.is_executing_oz_environment_startup_commands()
{
model
.block_list_mut()
.set_is_executing_oz_environment_startup_commands(false);
}
}
// Collapse the setup-commands summary, matching the oz first-exchange behavior.
self.ambient_agent_view_model.update(ctx, |model, ctx| {
model.set_setup_command_visibility(false, ctx);
});
// Force a fresh viewer size report to the sharer so the harness CLI (e.g.
// the claude TUI) starts at our terminal's actual dimensions instead of
// whatever the sandbox PTY was sized to during setup.
self.force_report_viewer_terminal_size(ctx);
ctx.notify();
}
AmbientAgentViewModelEvent::UpdatedSetupCommandVisibility => (),
}
}
pub(in crate::terminal::view) fn maybe_insert_setup_command_blocks(
&mut self,
block_id: &BlockId,
ctx: &mut ViewContext<Self>,
) {
if !FeatureFlag::CloudModeSetupV2.is_enabled() {
return;
}
if !is_cloud_agent_pre_first_exchange(
&self.ambient_agent_view_model,
&self.agent_view_controller,
ctx,
) {
return;
}
// For non-oz harness runs, transition out of the setup phase when the harness CLI
// starts (e.g. `claude --session-id …`). The block is the actual harness session
// and should NOT be classified as a setup command; the `HarnessCommandStarted`
// handler flips the block-list flag so the block renders like a normal CLI-agent
// session.
if self
.ambient_agent_view_model
.as_ref(ctx)
.is_third_party_harness()
&& self.active_block_matches_run_harness(ctx)
{
self.ambient_agent_view_model.update(ctx, |model, ctx| {
model.mark_harness_command_started(ctx);
});
return;
}
let Some(block_index) = self.model.lock().block_list().block_index_for_id(block_id) else {
return;
};
if !self
.ambient_agent_view_model
.as_ref(ctx)
.setup_command_state()
.did_execute_a_setup_command()
{
self.ambient_agent_view_model.update(ctx, |model, _| {
model
.setup_command_state_mut()
.set_did_execute_a_setup_command(true);
});
let setup_command_text = ctx.add_typed_action_view(|ctx| {
super::CloudModeSetupTextBlock::new(
self.ambient_agent_view_model.clone(),
self.agent_view_controller.clone(),
ctx,
)
});
self.insert_rich_content(
None,
setup_command_text,
None,
RichContentInsertionPosition::BeforeBlockIndex(block_index),
ctx,
);
}
let setup_command_block = ctx.add_typed_action_view(|ctx| {
super::CloudModeSetupCommandBlock::new(
block_id.clone(),
self.ambient_agent_view_model.clone(),
&self.model_events_handle,
self.model.clone(),
ctx,
)
});
ctx.subscribe_to_view(&setup_command_block, |me, _, event, _| {
let super::CloudModeSetupCommandBlockEvent::ToggleBlockVisibility(block_id) = event;
me.model
.lock()
.block_list_mut()
.toggle_visibility_of_block(block_id);
});
self.insert_rich_content(
None,
setup_command_block,
None,
RichContentInsertionPosition::BeforeBlockIndex(block_index),
ctx,
);
}
/// Enters agent view for a live shared-session viewer of a non-oz cloud run, so every
/// viewer lands in the same agent-view chrome regardless of which entry point opened the
/// conversation. Called from the `HarnessSelected` handler once the viewer has resolved
/// the run's harness asynchronously.
///
/// Transcript viewer entry is handled directly in `load_data_into_transcript_viewer` so
/// the snapshot block exists before we retag — we intentionally do not trigger that path
/// here.
///
/// The viewer-context guard is load-bearing: `HarnessSelected` also fires when the local
/// spawner picks a harness from the dropdown, and in that case the cloud-mode setup flow
/// handles agent view entry instead.
fn maybe_enter_agent_view_for_shared_third_party_viewer(
&mut self,
ctx: &mut ViewContext<Self>,
) {
if self
.agent_view_controller
.as_ref(ctx)
.agent_view_state()
.is_active()
{
return;
}
if !self
.ambient_agent_view_model
.as_ref(ctx)
.is_third_party_harness()
{
return;
}
if !self.is_shared_ambient_agent_session() {
return;
}
self.enter_agent_view_for_new_conversation(
None,
AgentViewEntryOrigin::ThirdPartyCloudAgent,
ctx,
);
let Some(vehicle_conversation_id) = self
.agent_view_controller
.as_ref(ctx)
.agent_view_state()
.active_conversation_id()
else {
return;
};
// Retag existing non-setup blocks so the harness content passes the agent view filter.
self.model
.lock()
.block_list_mut()
.attach_non_startup_blocks_to_conversation(vehicle_conversation_id);
// Retag rich content inserted in terminal mode (setup-commands summary, tombstone, …)
// so it stays visible under the vehicle conversation. Rich content with
// `agent_view_conversation_id == None` is hidden in full-screen agent view by
// `RichContentItem::should_hide_for_agent_view_state`.
let ids_to_retag: Vec<EntityId> = self
.rich_content_views
.iter()
.filter(|rc| rc.agent_view_conversation_id().is_none())
.map(|rc| rc.view_id())
.collect();
for view_id in ids_to_retag {
self.set_rich_content_agent_view_conversation_id(view_id, vehicle_conversation_id);
}
}
/// Returns `true` when the active block's command is the CLI for the run's configured
/// non-oz harness (e.g. `claude …` for [`Harness::Claude`]).
/// Used to detect the harness-start transition at `AfterBlockStarted` time. Unlike
/// `detect_cli_agent_from_model`, this does NOT gate on `is_active_and_long_running` —
/// we want to classify the block as the harness session as soon as it starts, before the
/// long-running timer would otherwise elapse.
fn active_block_matches_run_harness(&self, ctx: &AppContext) -> bool {
let command = self
.model
.lock()
.block_list()
.active_block()
.command_with_secrets_obfuscated(false);
let Some(cli_agent) = CLIAgent::detect(&command, None, None, ctx) else {
return false;
};
match self.ambient_agent_view_model.as_ref(ctx).selected_harness() {
Harness::Oz => false,
Harness::Claude => matches!(cli_agent, CLIAgent::Claude),
Harness::OpenCode => matches!(cli_agent, CLIAgent::OpenCode),
Harness::Gemini => matches!(cli_agent, CLIAgent::Gemini),
Harness::Unknown => false,
}
}
/// Enter cloud agent view from this existing session. Behavior depends on the current terminal state:
///
/// 1. Already in nested cloud mode with empty convo (setup/composing): ignore.
/// 2. Already in nested cloud mode with convo started: pop to parent terminal and start a
/// new cloud mode session there (siblings).
/// 3. Not in nested cloud mode: enter cloud mode from this terminal session.
pub(in crate::terminal::view) fn enter_cloud_agent_view(
&mut self,
initial_prompt: Option<String>,
ctx: &mut ViewContext<Self>,
) {
let ambient_agent_view = self.ambient_agent_view_model.as_ref(ctx);
let is_nested_cloud_mode =
ambient_agent_view.is_ambient_agent() && ambient_agent_view.has_parent_terminal();
// (1) If we're currently in an empty cloud mode session (setup/composing; no
// dispatched query yet), do not allow creating a new cloud mode session.
if is_nested_cloud_mode
&& (ambient_agent_view.is_in_setup()
|| ambient_agent_view.is_configuring_ambient_agent())
{
return;
}
if is_nested_cloud_mode {
// (2) Start a sibling cloud mode session at the terminal level.
let Some(pane_stack) = self
.pane_stack
.as_ref()
.and_then(|handle| handle.upgrade(ctx))
else {
log::warn!(
"Nested cloud mode has no pane stack; cannot pop to start sibling cloud mode session"
);
return;
};
if pane_stack.as_ref(ctx).depth() <= 1 {
log::warn!(
"Nested cloud mode pane stack depth <= 1; cannot pop to start sibling cloud mode session"
);
return;
}
pane_stack.update(ctx, |stack, ctx| {
stack.pop(ctx);
});
let active_view = pane_stack.as_ref(ctx).active_view().clone();
active_view.update(ctx, |view, ctx| {
view.enter_cloud_mode_from_session(initial_prompt, ctx);
});
ctx.notify();
return;
}
// (3) Enter cloud mode from this terminal session.
self.enter_cloud_mode_from_session(initial_prompt, ctx);
}
/// Enter cloud mode from this existing session with the given initial prompt.
///
/// If called from fullscreen agent view, this defers the cloud mode start until after the
/// agent view has exited so the resulting rich content is scoped to the terminal-level.
pub(in crate::terminal::view) fn enter_cloud_mode_from_session(
&mut self,
initial_prompt: Option<String>,
ctx: &mut ViewContext<Self>,
) {
if !(FeatureFlag::CloudMode.is_enabled()
&& FeatureFlag::CloudModeFromLocalSession.is_enabled())
{
return;
}
// If cloud mode is started from fullscreen agent view, we must ensure the resulting
// rich content (ambient agent entry block) is scoped to the terminal-level.
if FeatureFlag::AgentView.is_enabled()
&& self.agent_view_controller.as_ref(ctx).is_fullscreen()
{
let prompt = initial_prompt.clone();
self.set_pending_cloud_mode_start_callback(
Box::new(move |view, ctx| {
view.start_cloud_mode(None, prompt, ctx);
}),
ctx,
);
// Starting cloud mode from agent view is analogous to starting a new agent
// conversation: we exit without confirmation and continue after ExitedAgentView.
self.agent_view_controller.update(ctx, |controller, ctx| {
controller.exit_agent_view_without_confirmation(ctx);
});
return;
}
self.start_cloud_mode(None, initial_prompt, ctx);
}
/// Start a cloud mode session nested under this one.
///
/// If `spawn_request` is `Some`, the agent is immediately started. Otherwise, it can
/// further configured in the cloud mode session.
fn start_cloud_mode(
&mut self,
spawn_request: Option<SpawnAgentRequest>,
initial_prompt: Option<String>,
ctx: &mut ViewContext<Self>,
) {
let resources = TerminalViewResources {
tips_completed: self.tips_completed.clone(),
server_api: self.server_api.clone(),
model_event_sender: self.model_event_sender.clone(),
};
// TODO: Use self.size_info
let (terminal_view, terminal_manager) =
super::create_cloud_mode_view(resources, Vector2F::zero(), ctx.window_id(), ctx);
// Only insert an ambient agent entry block once the agent is actually dispatched.
// This avoids persisting an empty "New cloud agent" entry when the user enters cloud mode
// but exits without sending anything.
let ambient_agent_view_model = terminal_view.as_ref(ctx).ambient_agent_view_model().clone();
let terminal_view_weak = terminal_view.downgrade();
let terminal_manager_weak = terminal_manager.downgrade();
let pane_stack = self.pane_stack.clone();
let has_inserted_entry_block = Rc::new(Cell::new(false));
ctx.subscribe_to_model(&ambient_agent_view_model, move |me, _, event, ctx| {
if !matches!(event, AmbientAgentViewModelEvent::DispatchedAgent) {
return;
}
if has_inserted_entry_block.get() {
return;
}
has_inserted_entry_block.set(true);
let Some(pane_stack) = pane_stack.clone() else {
log::warn!(
"Pane stack not available; cannot insert ambient agent entry block for cloud mode"
);
return;
};
let Some(terminal_view) = terminal_view_weak.upgrade(ctx) else {
return;
};
let Some(terminal_manager) = terminal_manager_weak.upgrade(ctx) else {
return;
};
let block_terminal_view = terminal_view.clone();
let block_terminal_manager = terminal_manager.clone();
let block_handle = ctx.add_typed_action_view(|ctx| {
AmbientAgentEntryBlock::new(
block_terminal_view,
block_terminal_manager,
pane_stack.clone(),
ctx,
)
});
me.insert_rich_content(
None,
block_handle.clone(),
Some(RichContentMetadata::AmbientAgentBlock { block_handle }),
RichContentInsertionPosition::Append {
insert_below_long_running_block: false,
},
ctx,
);
});
let pane_config = self.pane_configuration.clone();
terminal_view.update(ctx, |view, ctx| {
view.set_pane_configuration(pane_config);
// Mark as having a parent since it's pushed onto an existing pane stack
view.ambient_agent_view_model.update(ctx, |model, _ctx| {
model.set_has_parent_terminal(true);
});
if let Some(request) = spawn_request {
// Spawn the agent immediately with the provided request.
view.enter_agent_view_for_new_conversation(
None,
AgentViewEntryOrigin::CloudAgent,
ctx,
);
view.ambient_agent_view_model.update(ctx, |model, ctx| {
model.spawn_agent_with_request(request, ctx);
});
} else {
// Enter setup mode for composing a prompt
view.enter_ambient_agent_setup(initial_prompt, ctx);
}
});
if let Some(pane_stack) = self.pane_stack.clone() {
if let Some(stack) = pane_stack.upgrade(ctx) {
stack.update(ctx, |stack, ctx| {
stack.push(terminal_manager, terminal_view, ctx);
});
} else {
log::warn!("Pane stack deallocated, cannot enter cloud mode");
}
} else {
log::warn!("Pane stack not available, cannot enter cloud mode");
}
send_telemetry_from_ctx!(
CloudAgentTelemetryEvent::EnteredCloudMode {
entry_point: CloudModeEntryPoint::LocalSession
},
ctx
);
}
/// Renders the ambient agent progress view based on agent progress.
pub(in crate::terminal::view) fn render_ambient_agent_progress(
&self,
appearance: &Appearance,
app: &AppContext,
) -> Box<dyn Element> {
let ambient_agent_model = self.ambient_agent_view_model.as_ref(app);
let Some(progress) = ambient_agent_model.agent_progress() else {
return Empty::new().finish();
};
// Show appropriate screen based on agent status
let ui_state = &ambient_agent_model.ui_state;
let screen = if ambient_agent_model.is_cancelled() {
// Show cancelled screen
render_cloud_mode_cancelled_screen(appearance)
} else if let Some(auth_url) = ambient_agent_model.github_auth_url() {
// Show GitHub auth required screen
render_cloud_mode_github_auth_required_screen(
auth_url,
appearance,
&ui_state.auth_button_mouse_state,
app,
)
} else if let Some(error_message) = ambient_agent_model.error_message() {
// Show error screen
render_cloud_mode_error_screen(
error_message,
appearance,
&ui_state.error_selection_handle,
&ui_state.error_selected_text,
app,
)
} else {
// Show loading screen - determine the message based on progress state
let message = 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)"
};
render_cloud_mode_loading_screen(
message,
appearance,
&ui_state.loading_shimmer_handle,
&ui_state.tip_model,
app,
)
};
// Center the screen within the terminal view
Align::new(screen).finish()
}
/// Handles events from the first-time cloud agent setup view.
pub(in crate::terminal::view) fn handle_first_time_cloud_agent_setup_event(
&mut self,
event: &super::FirstTimeCloudAgentSetupViewEvent,
ctx: &mut ViewContext<Self>,
) {
match event {
super::FirstTimeCloudAgentSetupViewEvent::Cancelled => {
// Exit agent view (pops from nav stack)
self.exit_agent_view(ctx);
}
super::FirstTimeCloudAgentSetupViewEvent::EnvironmentCreated => {
// Set the environment on the ambient agent view model
self.ambient_agent_view_model.update(ctx, |model, ctx| {
// Transition from Setup to Composing
model.enter_composing_from_setup(ctx);
});
// Focus the input box so user can start typing
self.focus_input_box(ctx);
}
}
}
/// Fetches task data and updates the cloud mode details panel.
pub(in crate::terminal::view) fn fetch_and_update_cloud_mode_details_panel(
&mut self,
ctx: &mut ViewContext<Self>,
) {
let Some(task_id) = self.ambient_agent_task_id_for_details_panel(ctx) else {
log::warn!("fetch_and_update_cloud_mode_details_panel called without task_id");
return;
};
let task = crate::ai::agent_conversations_model::AgentConversationsModel::handle(ctx)
.update(ctx, |model, ctx| {
model.get_or_async_fetch_task_data(&task_id, ctx)
});
let data = task
.as_ref()
.map(|task| ConversationDetailsData::from_task(task, None, None, ctx))
.unwrap_or_else(|| ConversationDetailsData::from_task_id(task_id));
self.cloud_mode_details_panel.update(ctx, |panel, ctx| {
panel.set_conversation_details(data, ctx);
});
}
/// Auto-opens the cloud mode details panel once.
/// This is used for local cloud mode sessions (after SessionReady) and shared ambient sessions (after join).
pub(in crate::terminal::view) fn maybe_auto_open_cloud_mode_details_panel(
&mut self,
ctx: &mut ViewContext<Self>,
) {
if self.has_auto_opened_cloud_mode_details_panel {
return;
}
self.is_cloud_mode_details_panel_open = true;
self.has_auto_opened_cloud_mode_details_panel = true;
self.fetch_and_update_cloud_mode_details_panel(ctx);
ctx.notify();
}
}