Skip to content
Open
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
2 changes: 1 addition & 1 deletion app/src/ai/blocklist/action_model.rs
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ pub use execute::{
ReadFileContextResult, RequestFileEditsExecutor, RequestFileEditsFormatKind,
RequestFileEditsTelemetryEvent, RunAgentsExecutor, RunAgentsExecutorEvent,
RunAgentsSpawningSnapshot, ShellCommandExecutor, ShellCommandExecutorEvent, StartAgentExecutor,
StartAgentExecutorEvent, StartAgentRequest, StartAgentRequestId,
StartAgentExecutorEvent, StartAgentOutcome, StartAgentRequest, StartAgentRequestId,
};
use futures::future::{join_all, BoxFuture};
use itertools::Itertools;
Expand Down
3 changes: 2 additions & 1 deletion app/src/ai/blocklist/action_model/execute.rs
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,8 @@ pub use send_message::SendMessageToAgentExecutor;
use serde::{Deserialize, Serialize};
pub use shell_command::{ShellCommandExecutor, ShellCommandExecutorEvent};
pub use start_agent::{
StartAgentExecutor, StartAgentExecutorEvent, StartAgentRequest, StartAgentRequestId,
StartAgentExecutor, StartAgentExecutorEvent, StartAgentOutcome, StartAgentRequest,
StartAgentRequestId,
};
use start_recording::StartRecordingExecutor;
use stop_recording::StopRecordingExecutor;
Expand Down
93 changes: 93 additions & 0 deletions app/src/ai/blocklist/child_agent_launch.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
//! Frontend-neutral preparation and settings propagation for local Oz children.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this is mostly just code moved from GUI-specific places into shared helpers

#[cfg(not(target_family = "wasm"))]
use std::future::Future;

use warpui::{AppContext, EntityId, SingletonEntity as _};
#[cfg(not(target_family = "wasm"))]
use {
crate::ai::ambient_agents::task::normalize_orchestrator_agent_name,
crate::ai::ambient_agents::{AgentConfigSnapshot, AmbientAgentTaskId},
crate::server::server_api::ServerApiProvider,
};

use crate::ai::llms::{LLMId, LLMPreferences};
use crate::AIExecutionProfilesModel;

/// Server-side state prepared before a frontend creates the child's surface.
#[cfg(not(target_family = "wasm"))]
pub struct PreparedLocalOzChildLaunch {
pub task_id: AmbientAgentTaskId,
pub conversation_name: String,
}

/// Creates the server task row shared by the GUI hidden-pane and TUI
/// background-session launch paths.
#[cfg(not(target_family = "wasm"))]
pub fn prepare_local_oz_child_launch(
name: &str,
prompt: &str,
parent_run_id: Option<&str>,
ctx: &AppContext,
) -> impl Future<Output = anyhow::Result<PreparedLocalOzChildLaunch>> + 'static {
let ai_client = ServerApiProvider::as_ref(ctx).get_ai_client();
let agent_name = normalize_orchestrator_agent_name(name);
let conversation_name = agent_name.clone().unwrap_or_default();
let prompt = prompt.to_owned();
let parent_run_id = parent_run_id.map(str::to_owned);
async move {
let task_id = ai_client
.create_agent_task(
prompt,
None,
parent_run_id,
Some(AgentConfigSnapshot {
name: agent_name,
..Default::default()
}),
)
.await?;
Ok(PreparedLocalOzChildLaunch {
task_id,
conversation_name,
})
}
}

/// Copies the parent's execution profile and effective base model to a child
/// surface before its first request is sent.
pub fn inherit_child_agent_settings(
parent_surface_id: EntityId,
child_surface_id: EntityId,
ctx: &mut AppContext,
) {
let parent_profile_id = *AIExecutionProfilesModel::as_ref(ctx)
.active_profile(Some(parent_surface_id), ctx)
.id();
AIExecutionProfilesModel::handle(ctx).update(ctx, |profiles, ctx| {
profiles.set_active_profile(child_surface_id, parent_profile_id, ctx);
});

let parent_base_model_id = LLMPreferences::as_ref(ctx)
.get_active_base_model(ctx, Some(parent_surface_id))
.id
.clone();
LLMPreferences::handle(ctx).update(ctx, |preferences, ctx| {
preferences.update_preferred_agent_mode_llm(&parent_base_model_id, child_surface_id, ctx);
});
}

/// Applies a non-empty run-wide model override after parent settings have
/// been inherited.
pub fn apply_child_agent_model_override(
child_surface_id: EntityId,
model_id: Option<&str>,
ctx: &mut AppContext,
) {
let Some(model_id) = model_id.map(str::trim).filter(|id| !id.is_empty()) else {
return;
};
let model_id = LLMId::from(model_id);
LLMPreferences::handle(ctx).update(ctx, |preferences, ctx| {
preferences.set_agent_mode_llm_override(child_surface_id, model_id, ctx);
});
}
4 changes: 4 additions & 0 deletions app/src/ai/blocklist/history_model.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2180,6 +2180,10 @@ impl BlocklistAIHistoryModel {

self.all_conversations_metadata.remove(&conversation_id);
self.conversations_by_id.remove(&conversation_id);
self.children_by_parent.retain(|_, child_ids| {
child_ids.retain(|child_id| *child_id != conversation_id);
!child_ids.is_empty()
});
Comment on lines +2183 to +2186

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ [IMPORTANT] This only removes the deleted conversation when it appears as a child, but leaves a stale entry when the deleted conversation is itself a parent; remove that parent key before pruning other parents so child_conversation_ids_of(&removed_parent) cannot return orphaned children.

Suggested change
self.children_by_parent.retain(|_, child_ids| {
child_ids.retain(|child_id| *child_id != conversation_id);
!child_ids.is_empty()
});
self.children_by_parent.remove(&conversation_id);
self.children_by_parent.retain(|_, child_ids| {
child_ids.retain(|child_id| *child_id != conversation_id);
!child_ids.is_empty()
});

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Seems valid?


if let Some(terminal_surface_id) = terminal_surface_id {
if self
Expand Down
26 changes: 26 additions & 0 deletions app/src/ai/blocklist/history_model_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1926,6 +1926,32 @@ fn test_set_parent_multiple_children() {
});
}

#[test]
fn test_remove_child_conversation_cleans_parent_index() {
App::test((), |mut app| async move {
let terminal_view_id = EntityId::new();
let history_model =
app.add_singleton_model(|_| BlocklistAIHistoryModel::new(vec![], vec![], &[]));
let parent_id = history_model.update(&mut app, |model, ctx| {
model.start_new_conversation(terminal_view_id, false, false, false, ctx)
});
let child_id = history_model.update(&mut app, |model, ctx| {
let child_id = model.start_new_conversation(terminal_view_id, false, false, false, ctx);
model.set_parent_for_conversation(child_id, parent_id);
child_id
});

history_model.update(&mut app, |model, ctx| {
model.remove_conversation(child_id, terminal_view_id, ctx);
});

history_model.read(&app, |model, _| {
assert!(model.conversation(&child_id).is_none());
assert!(model.child_conversation_ids_of(&parent_id).is_empty());
});
});
}

#[test]
fn test_child_conversation_ids_of_unknown_parent() {
App::test((), |app| async move {
Expand Down
18 changes: 16 additions & 2 deletions app/src/ai/blocklist/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
mod action_model;
pub mod agent_view;
pub mod block;
mod child_agent_launch;
pub mod code_block;
mod context_model;
mod controller;
Expand Down Expand Up @@ -49,19 +50,32 @@ pub use action_model::RequestFileEditsExecutor;
#[cfg_attr(target_family = "wasm", allow(unused_imports))]
pub(crate) use action_model::{
apply_edits, read_local_file_context, FileReadResult, ReadFileContextResult,
RequestFileEditsFormatKind, StartAgentExecutor, StartAgentExecutorEvent, StartAgentRequest,
StartAgentRequestId,
RequestFileEditsFormatKind,
};
pub use action_model::{
BlocklistAIActionEvent, BlocklistAIActionModel, ShellCommandExecutor, ShellCommandExecutorEvent,
};
// Consumed by `tui_export` for the `warp_tui` frontend.
#[cfg_attr(not(feature = "tui"), allow(unused_imports))]
pub use action_model::{RunAgentsExecutor, RunAgentsExecutorEvent, RunAgentsSpawningSnapshot};
// Consumed by `tui_export` for the `warp_tui` frontend's child-agent
// materializer, in addition to the GUI pane-group dispatch.
#[cfg_attr(
any(target_family = "wasm", not(feature = "tui")),
allow(unused_imports)
)]
pub use action_model::{
StartAgentExecutor, StartAgentExecutorEvent, StartAgentOutcome, StartAgentRequest,
StartAgentRequestId,
};
#[cfg(any(test, feature = "integration_tests"))]
pub(crate) use block::model::testing::FakeAIBlockModel;
pub(crate) use block::{init, model, AIBlock, AIBlockEvent, RequestedEditResolution};
pub use block::{keyboard_navigable_buttons, toggleable_items};
pub use child_agent_launch::{apply_child_agent_model_override, inherit_child_agent_settings};
#[cfg(not(target_family = "wasm"))]
#[cfg_attr(not(feature = "tui"), allow(unused_imports))]
pub use child_agent_launch::{prepare_local_oz_child_launch, PreparedLocalOzChildLaunch};
#[cfg(not(feature = "tui"))]
pub(crate) use context_model::block_context_from_terminal_model;
#[cfg(feature = "tui")]
Expand Down
38 changes: 28 additions & 10 deletions app/src/ai/llms.rs
Original file line number Diff line number Diff line change
Expand Up @@ -854,14 +854,6 @@ impl LLMPreferences {
app: &AppContext,
terminal_view_id: Option<EntityId>,
) -> &LLMInfo {
// In the TUI, the file-backed `agents.model` setting is the source of
// truth for the base model: it overrides both per-surface overrides
// and the cloud-synced execution profile, keeping the TUI's TOML file
// the single place the model is configured.
if settings_mode == settings::SettingsMode::Tui {
return self.tui_agent_model_info(AISettings::as_ref(app).agent_model.value(), app);
}

if let Some(terminal_view_id) = terminal_view_id {
let raw_override = self.base_llm_for_terminal_view.get(&terminal_view_id);
if let Some(llm_id) = raw_override {
Expand All @@ -873,6 +865,12 @@ impl LLMPreferences {
}
}

// In the TUI, the file-backed `agents.model` setting is the default
// for every surface. Explicit per-surface overrides still take
// precedence for orchestrated children launched with a model override.
if settings_mode == settings::SettingsMode::Tui {
return self.tui_agent_model_info(AISettings::as_ref(app).agent_model.value(), app);
}
let profile = AIExecutionProfilesModel::as_ref(app).active_profile(terminal_view_id, app);

profile
Expand Down Expand Up @@ -1497,8 +1495,8 @@ impl LLMPreferences {
.is_some()
} else {
self.base_llm_for_terminal_view
.insert(terminal_view_id, preferred_llm_id.clone());
true
.insert(terminal_view_id, preferred_llm_id.clone())
!= Some(preferred_llm_id.clone())
};

if changed {
Expand All @@ -1507,6 +1505,26 @@ impl LLMPreferences {
}
}

/// Sets an explicit runtime override without normalizing it against the
/// execution profile. Orchestrated child runs use this because a requested
/// model equal to the profile default must still override the TUI's
/// file-backed model setting.
pub(crate) fn set_agent_mode_llm_override(

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Seems like emitting UpdatedActiveAgentModeLLM triggers a bunch of handlers downstream. ooc why do we need this specifically for TUI?

&mut self,
terminal_view_id: EntityId,
model_id: LLMId,
ctx: &mut ModelContext<Self>,
) {
if self
.base_llm_for_terminal_view
.insert(terminal_view_id, model_id.clone())
!= Some(model_id)
{
self.trigger_snapshot_save(ctx);
ctx.emit(LLMPreferencesEvent::UpdatedActiveAgentModeLLM);
}
}

/// Copies the raw per-pane Agent Mode override from `source_terminal_view_id`
/// onto `new_terminal_view_id`, removing any existing override when the
/// source has none. Combined with copying the source's execution profile,
Expand Down
23 changes: 23 additions & 0 deletions app/src/ai/llms_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -932,6 +932,29 @@ fn tui_agent_model_known_id_resolves_to_that_model() {
});
}

#[test]
fn tui_surface_override_precedes_file_backed_default() {
App::test((), |app| async move {
app.add_singleton_model(|_| AuthStateProvider::new_for_test());
app.add_singleton_model(UserWorkspaces::default_mock);
app.read(|app_ctx| {
let surface_id = EntityId::new();
let mut preferences = preferences_for_tui_tests();
preferences
.base_llm_for_terminal_view
.insert(surface_id, LLMId::from("auto"));

let info = preferences.get_preferred_base_model_for_settings_mode(
settings::SettingsMode::Tui,
app_ctx,
Some(surface_id),
);

assert_eq!(info.id.as_str(), "auto");
});
});
}

#[test]
fn tui_agent_model_unknown_id_falls_back_to_the_default_model() {
tui_agent_model_test(|preferences, app| {
Expand Down
2 changes: 1 addition & 1 deletion app/src/global_resource_handles.rs
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@ pub struct GlobalResourceHandles {
}

impl GlobalResourceHandles {
#[cfg(any(test, feature = "integration_tests"))]
#[cfg(any(test, feature = "integration_tests", feature = "test-util"))]
pub fn mock(app: &mut warpui::App) -> Self {
let referral_theme_status = app.add_model(ReferralThemeStatus::new);
let user_default_shell_unsupported_banner_model_handle =
Expand Down
48 changes: 10 additions & 38 deletions app/src/pane_group/child_agent/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,12 +14,12 @@ use crate::ai::agent::RenderableAIError;
use crate::ai::ambient_agents::AmbientAgentTaskId;
use crate::ai::attachment_utils::attachments_download_dir;
use crate::ai::blocklist::agent_view::AgentViewEntryOrigin;
use crate::ai::blocklist::{BlocklistAIHistoryModel, StartAgentRequestId};
use crate::ai::llms::LLMPreferences;
use crate::ai::blocklist::{
inherit_child_agent_settings, BlocklistAIHistoryModel, StartAgentRequestId,
};
use crate::pane_group::{PaneGroup, PaneId};
use crate::terminal::shared_session::IsSharedSessionCreator;
use crate::terminal::TerminalView;
use crate::AIExecutionProfilesModel;

pub(crate) struct HiddenChildAgentConversation {
pub terminal_view: ViewHandle<TerminalView>,
Expand Down Expand Up @@ -75,40 +75,6 @@ pub(crate) fn apply_hidden_child_agent_task_context(
});
}

fn propagate_parent_agent_settings(
group: &PaneGroup,
parent_pane_id: PaneId,
child_terminal_view_id: EntityId,
ctx: &mut ViewContext<PaneGroup>,
) {
let Some(parent_terminal_view) = group.terminal_view_from_pane_id(parent_pane_id, ctx) else {
log::warn!(
"Could not find parent terminal view for pane {parent_pane_id:?}; child will use default AI profile"
);
return;
};

let parent_view_id = parent_terminal_view.id();
let parent_profile_id = *AIExecutionProfilesModel::as_ref(ctx)
.active_profile(Some(parent_view_id), ctx)
.id();
AIExecutionProfilesModel::handle(ctx).update(ctx, |profiles, ctx| {
profiles.set_active_profile(child_terminal_view_id, parent_profile_id, ctx);
});

let parent_base_model_id = LLMPreferences::as_ref(ctx)
.get_active_base_model(ctx, Some(parent_view_id))
.id
.clone();
LLMPreferences::handle(ctx).update(ctx, |llm_prefs, ctx| {
llm_prefs.update_preferred_agent_mode_llm(
&parent_base_model_id,
child_terminal_view_id,
ctx,
);
});
}

fn start_new_child_conversation(
terminal_view_id: EntityId,
name: String,
Expand Down Expand Up @@ -154,7 +120,13 @@ pub(crate) fn create_hidden_child_agent_conversation(
};

let terminal_view_id = new_terminal_view.id();
propagate_parent_agent_settings(group, parent_pane_id, terminal_view_id, ctx);
if let Some(parent_terminal_view) = group.terminal_view_from_pane_id(parent_pane_id, ctx) {
inherit_child_agent_settings(parent_terminal_view.id(), terminal_view_id, ctx);
} else {
log::warn!(
"Could not find parent terminal view for pane {parent_pane_id:?}; child will use default AI profile"
);
}
if let Some(task_context) = task_context.as_ref() {
apply_hidden_child_agent_task_context(&new_terminal_view, task_context, ctx);
}
Expand Down
Loading