Skip to content
Merged
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
78 changes: 62 additions & 16 deletions app/src/terminal/view/ambient_agent/model.rs
Original file line number Diff line number Diff line change
Expand Up @@ -201,6 +201,10 @@ pub struct AmbientAgentViewModel {

/// Selected cloud environment to launch the ambient agent with.
environment_id: Option<SyncId>,
/// True when `environment_id` came from an existing run config rather than from local
/// environment selection/defaulting. Existing runs may reference an environment before the
/// local CloudModel has loaded it, so initial-load validation should not clear it.
environment_id_from_viewed_task: bool,

/// Handle for the periodic timer that updates progress durations.
progress_timer_handle: Option<SpawnedFutureHandle>,
Expand Down Expand Up @@ -296,6 +300,7 @@ impl AmbientAgentViewModel {
request: None,
terminal_view_id,
environment_id: None,
environment_id_from_viewed_task: false,
progress_timer_handle: None,
ui_state,
setup_commands_state: Default::default(),
Expand Down Expand Up @@ -401,6 +406,9 @@ impl AmbientAgentViewModel {
/// If the environment no longer exists, clears the selection.
fn validate_environment_after_initial_load(&mut self, ctx: &mut ModelContext<Self>) {
if let Some(id) = &self.environment_id {
if self.environment_id_from_viewed_task {
return;
}
if CloudAmbientAgentEnvironment::get_by_id(id, ctx).is_none() {
log::warn!(
"Environment {id:?} no longer exists after initial load, clearing selection"
Expand Down Expand Up @@ -730,6 +738,7 @@ impl AmbientAgentViewModel {
}
}
self.environment_id = environment_id;
self.environment_id_from_viewed_task = false;
ctx.emit(AmbientAgentViewModelEvent::EnvironmentSelected);
}

Expand Down Expand Up @@ -872,22 +881,7 @@ impl AmbientAgentViewModel {
async move { ai_client.get_ambient_agent_task(&task_id).await },
|me, result, ctx| match result {
Ok(task) => {
let snapshot = task.agent_config_snapshot.as_ref();
let harness_config = snapshot.and_then(|s| s.harness.as_ref());
let environment_id = snapshot
.and_then(|s| s.environment_id.as_deref())
.and_then(|id| ServerId::try_from(id).ok())
.map(SyncId::ServerId);
let harness = harness_config
.map(|h| h.harness_type)
.unwrap_or(Harness::Oz);
let harness_model_id = harness_config.and_then(|h| h.model_id.clone());
let harness_reasoning_level =
harness_config.and_then(|h| h.reasoning_level.clone());

me.set_environment_id(environment_id, ctx);
me.set_harness(harness, ctx);
me.set_harness_model_selection(harness_model_id, harness_reasoning_level, ctx);
me.apply_viewed_task_config_snapshot(task.agent_config_snapshot.as_ref(), ctx);
ctx.emit(AmbientAgentViewModelEvent::ViewerHarnessResolved);
}
Err(err) => {
Expand All @@ -898,6 +892,56 @@ impl AmbientAgentViewModel {
);
}

/// Applies the run configuration for an existing shared ambient session.
///
/// Viewed sessions can join before Warp Drive has loaded the referenced environment object,
/// especially on web. Preserve the server-provided environment ID anyway so the selector does
/// not fall back to an unrelated default while waiting for the environment object to arrive.
fn apply_viewed_task_config_snapshot(
&mut self,
snapshot: Option<&AgentConfigSnapshot>,
ctx: &mut ModelContext<Self>,
) {
let environment_id = snapshot
.and_then(|s| s.environment_id.as_deref())
.and_then(|id| ServerId::try_from(id).ok())
.map(SyncId::ServerId);
self.set_environment_id_from_viewed_task(environment_id, ctx);

if let Some(model_id) = snapshot.and_then(|s| s.model_id.as_deref()) {
LLMPreferences::handle(ctx).update(ctx, |prefs, ctx| {
prefs.update_preferred_agent_mode_llm(
&LLMId::from(model_id),
self.terminal_view_id,
ctx,
)
});
}

let harness_config = snapshot.and_then(|s| s.harness.as_ref());
let harness = harness_config
.map(|h| h.harness_type)
.unwrap_or(Harness::Oz);
let harness_model_id = harness_config.and_then(|h| h.model_id.clone());
let harness_reasoning_level = harness_config.and_then(|h| h.reasoning_level.clone());

self.set_harness(harness, ctx);
self.set_harness_model_selection(harness_model_id, harness_reasoning_level, ctx);
}

fn set_environment_id_from_viewed_task(
&mut self,
environment_id: Option<SyncId>,
ctx: &mut ModelContext<Self>,
) {
if self.environment_id == environment_id {
return;
}
self.environment_id_from_viewed_task = environment_id.is_some();
self.environment_id = environment_id;
ctx.emit(AmbientAgentViewModelEvent::EnvironmentSelected);
}

pub fn record_ambient_execution_ended(
&mut self,
session_id: SessionId,
Expand Down Expand Up @@ -988,6 +1032,7 @@ impl AmbientAgentViewModel {
pub fn reset_for_new_cloud_prompt(&mut self, ctx: &mut ModelContext<Self>) {
self.status = Status::Composing;
self.environment_id = None;
self.environment_id_from_viewed_task = false;
self.task_id = None;
self.conversation_id = None;
self.harness_model_id = None;
Expand Down Expand Up @@ -1106,6 +1151,7 @@ impl AmbientAgentViewModel {
.as_deref()
.and_then(|id| ServerId::try_from(id).ok())
.map(SyncId::ServerId);
self.environment_id_from_viewed_task = false;

if let Some(model_id) = config.model_id.as_deref() {
LLMPreferences::handle(ctx).update(ctx, |prefs, ctx| {
Expand Down
58 changes: 58 additions & 0 deletions app/src/terminal/view/ambient_agent/model_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ use warpui::{App, EntityId};

use super::*;
use crate::ai::blocklist::handoff::HandoffLaunchAttachments;
use crate::ai::llms::LLMPreferences;
use crate::test_util::terminal::initialize_app_for_terminal_view;
use url::Url;

Expand Down Expand Up @@ -77,6 +78,10 @@ fn retry_request(prompt: impl Into<String>) -> SpawnAgentRequest {
}
}

fn test_environment_id() -> ServerId {
ServerId::from(123)
}

#[test]
fn github_auth_url_for_initial_run_includes_focus_cloud_mode_next() {
App::test((), |mut app| async move {
Expand Down Expand Up @@ -165,6 +170,59 @@ fn github_auth_completed_retries_stored_initial_run_request() {
});
}

#[test]
fn viewed_task_config_preserves_environment_before_cloud_model_load() {
App::test((), |mut app| async move {
initialize_app_for_terminal_view(&mut app);
let model = add_model(&mut app);
let environment_id = test_environment_id();

model.update(&mut app, |model, ctx| {
model.apply_viewed_task_config_snapshot(
Some(&AgentConfigSnapshot {
environment_id: Some(environment_id.to_string()),
..Default::default()
}),
ctx,
);
model.validate_environment_after_initial_load(ctx);
});

model.read(&app, |model, _| {
assert_eq!(
model.selected_environment_id(),
Some(&SyncId::ServerId(environment_id))
);
});
});
}

#[test]
fn viewed_task_config_applies_oz_model_override() {
App::test((), |mut app| async move {
initialize_app_for_terminal_view(&mut app);
let model = add_model(&mut app);
let terminal_view_id = model.read(&app, |model, _| model.terminal_view_id);

model.update(&mut app, |model, ctx| {
model.apply_viewed_task_config_snapshot(
Some(&AgentConfigSnapshot {
model_id: Some("model-from-run".to_string()),
..Default::default()
}),
ctx,
);
});

let override_value = model.read(&app, |_, app| {
LLMPreferences::as_ref(app)
.get_base_llm_override(terminal_view_id)
.expect("viewed run model should be stored as a pane override")
});
assert_eq!(override_value, "\"model-from-run\"");
});
}

#[test]
fn followup_github_auth_does_not_reuse_stored_initial_request() {
App::test((), |mut app| async move {
Expand Down
Loading