ACP work
This commit is contained in:
@@ -30,6 +30,7 @@ use base64::Engine as _;
|
||||
use chrono::Duration;
|
||||
use cli_controller::{CLISubagentController, CLISubagentEvent};
|
||||
use find::FindState;
|
||||
use galaxy_agent_core::RuntimeActivityStatus;
|
||||
use galaxy_core::features::FeatureFlag;
|
||||
use galaxy_core::ui::theme::color::internal_colors;
|
||||
use galaxy_core::ui::theme::Fill;
|
||||
@@ -818,6 +819,27 @@ impl CollapsibleElementState {
|
||||
}
|
||||
}
|
||||
|
||||
fn sync_runtime_activity(&mut self, is_streaming: bool, is_finished: bool, has_output: bool) {
|
||||
if is_streaming
|
||||
&& has_output
|
||||
&& !self.user_toggled_while_streaming
|
||||
&& matches!(self.expansion_state, CollapsibleExpansionState::Collapsed)
|
||||
{
|
||||
self.expand();
|
||||
}
|
||||
|
||||
self.sync_finished_state(is_finished);
|
||||
if is_finished {
|
||||
if let CollapsibleExpansionState::Expanded {
|
||||
scroll_pinned_to_bottom,
|
||||
..
|
||||
} = &mut self.expansion_state
|
||||
{
|
||||
*scroll_pinned_to_bottom = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Applies orchestration message display behavior after streaming finishes.
|
||||
fn finish_orchestration_message(&mut self, display_mode: OrchestrationMessageDisplayMode) {
|
||||
let should_auto_collapse = self.should_auto_collapse_on_finish();
|
||||
@@ -2323,6 +2345,32 @@ impl AIBlock {
|
||||
|
||||
// Register element state for reasoning messages and track summarization timing.
|
||||
for message in &output.messages {
|
||||
if let AIAgentOutputMessageType::RuntimeActivity(activity) = &message.message {
|
||||
let is_streaming = matches!(
|
||||
activity.status,
|
||||
Some(RuntimeActivityStatus::Pending | RuntimeActivityStatus::InProgress)
|
||||
);
|
||||
let is_finished = matches!(
|
||||
activity.status,
|
||||
Some(RuntimeActivityStatus::Completed | RuntimeActivityStatus::Failed)
|
||||
);
|
||||
let has_output = activity
|
||||
.output
|
||||
.as_deref()
|
||||
.is_some_and(|output| !output.is_empty());
|
||||
let state = self
|
||||
.collapsible_block_states
|
||||
.entry(message.id.clone())
|
||||
.or_insert_with(|| {
|
||||
if is_streaming && has_output {
|
||||
CollapsibleElementState::default()
|
||||
} else {
|
||||
CollapsibleElementState::collapsed()
|
||||
}
|
||||
});
|
||||
state.sync_runtime_activity(is_streaming, is_finished, has_output);
|
||||
}
|
||||
|
||||
if let AIAgentOutputMessageType::Reasoning {
|
||||
finished_duration, ..
|
||||
} = &message.message
|
||||
@@ -2608,6 +2656,7 @@ impl AIBlock {
|
||||
| AIAgentOutputMessageType::Reasoning { .. }
|
||||
| AIAgentOutputMessageType::Summarization { .. }
|
||||
| AIAgentOutputMessageType::Subagent(_)
|
||||
| AIAgentOutputMessageType::RuntimeActivity(_)
|
||||
| AIAgentOutputMessageType::Action(_)
|
||||
| AIAgentOutputMessageType::TodoOperation(_)
|
||||
| AIAgentOutputMessageType::WebSearch(_)
|
||||
|
||||
@@ -1641,7 +1641,9 @@ fn should_retain_task_output_message(
|
||||
|| (is_latest_exchange
|
||||
&& matches!(
|
||||
message,
|
||||
AIAgentOutputMessageType::Action(_) | AIAgentOutputMessageType::WebSearch(_)
|
||||
AIAgentOutputMessageType::Action(_)
|
||||
| AIAgentOutputMessageType::RuntimeActivity(_)
|
||||
| AIAgentOutputMessageType::WebSearch(_)
|
||||
))
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
use std::time::Duration;
|
||||
|
||||
use galaxy_agent_core::RuntimeActivity;
|
||||
use galaxy_terminal::model::escape_sequences;
|
||||
|
||||
use super::{get_action_icon, get_action_loading_text, should_retain_task_output_message};
|
||||
@@ -58,4 +59,13 @@ fn transcript_retains_prior_text_but_only_latest_tool_activity() {
|
||||
});
|
||||
assert!(!should_retain_task_output_message(&poll, false));
|
||||
assert!(should_retain_task_output_message(&poll, true));
|
||||
|
||||
let runtime_activity = AIAgentOutputMessageType::RuntimeActivity(RuntimeActivity {
|
||||
id: "acp-tool".to_owned(),
|
||||
title: "Inspect repository".to_owned(),
|
||||
status: None,
|
||||
output: None,
|
||||
});
|
||||
assert!(!should_retain_task_output_message(&runtime_activity, false));
|
||||
assert!(should_retain_task_output_message(&runtime_activity, true));
|
||||
}
|
||||
|
||||
@@ -16,6 +16,7 @@ use ai::agent::action::{
|
||||
};
|
||||
use ai::agent::file_locations::group_file_contexts_for_display;
|
||||
use ai::skills::{ParsedSkill, SkillReference};
|
||||
use galaxy_agent_core::{RuntimeActivity, RuntimeActivityStatus};
|
||||
use galaxy_core::channel::ChannelState;
|
||||
use galaxy_core::ui::theme::color::internal_colors;
|
||||
use galaxy_util::local_or_remote_path::LocalOrRemotePath;
|
||||
@@ -400,6 +401,21 @@ pub(super) fn render(props: Props, app: &AppContext) -> Box<dyn Element> {
|
||||
} if !are_all_text_sections_empty(sections) => {
|
||||
text_section_index += sections.len();
|
||||
}
|
||||
AIAgentOutputMessageType::RuntimeActivity(activity) => {
|
||||
if !matches!(
|
||||
activity.status,
|
||||
Some(RuntimeActivityStatus::Completed)
|
||||
| Some(RuntimeActivityStatus::Failed)
|
||||
) {
|
||||
should_render_footer = false;
|
||||
should_render_suggestions = false;
|
||||
}
|
||||
if let Some(rendered_activity) =
|
||||
render_runtime_activity(output_message, activity, props, app)
|
||||
{
|
||||
output_items.add_child(rendered_activity);
|
||||
}
|
||||
}
|
||||
AIAgentOutputMessageType::Action(AIAgentAction {
|
||||
action: AIAgentActionType::RequestCommandOutput { .. },
|
||||
id,
|
||||
@@ -1262,6 +1278,119 @@ pub(super) fn render(props: Props, app: &AppContext) -> Box<dyn Element> {
|
||||
output_items.finish()
|
||||
}
|
||||
|
||||
fn render_runtime_activity(
|
||||
output_message: &AIAgentOutputMessage,
|
||||
activity: &RuntimeActivity,
|
||||
props: Props,
|
||||
app: &AppContext,
|
||||
) -> Option<Box<dyn Element>> {
|
||||
let state = props.collapsible_block_states.get(&output_message.id)?;
|
||||
let appearance = Appearance::as_ref(app);
|
||||
let theme = appearance.theme();
|
||||
let text_color = blended_colors::text_main(theme, theme.background());
|
||||
let output = activity
|
||||
.output
|
||||
.as_deref()
|
||||
.filter(|output| !output.is_empty());
|
||||
let is_expanded = matches!(
|
||||
state.expansion_state,
|
||||
CollapsibleExpansionState::Expanded { .. }
|
||||
);
|
||||
let mut content = Flex::column().with_cross_axis_alignment(CrossAxisAlignment::Stretch);
|
||||
|
||||
let title = Text::new(
|
||||
activity.title.clone(),
|
||||
appearance.monospace_font_family(),
|
||||
appearance.monospace_font_size(),
|
||||
)
|
||||
.with_color(text_color)
|
||||
.with_selectable(false)
|
||||
.finish();
|
||||
if output.is_some() {
|
||||
let chevron = if is_expanded {
|
||||
Icon::ChevronDown
|
||||
} else {
|
||||
Icon::ChevronRight
|
||||
};
|
||||
let icon_sz = icon_size(app);
|
||||
let message_id = output_message.id.clone();
|
||||
let mouse_state = state.expansion_toggle_mouse_state.clone();
|
||||
let header = Hoverable::new(mouse_state, move |_| {
|
||||
Flex::row()
|
||||
.with_cross_axis_alignment(CrossAxisAlignment::Center)
|
||||
.with_child(Shrinkable::new(1., title).finish())
|
||||
.with_child(
|
||||
Container::new(
|
||||
ConstrainedBox::new(chevron.to_galaxyui_icon(text_color.into()).finish())
|
||||
.with_width(icon_sz)
|
||||
.with_height(icon_sz)
|
||||
.finish(),
|
||||
)
|
||||
.with_margin_left(6.)
|
||||
.finish(),
|
||||
)
|
||||
.finish()
|
||||
})
|
||||
.with_cursor(Cursor::PointingHand)
|
||||
.on_click(move |ctx, _, _| {
|
||||
ctx.dispatch_typed_action(AIBlockAction::ToggleCollapsibleBlockExpanded(
|
||||
message_id.clone(),
|
||||
));
|
||||
});
|
||||
content.add_child(
|
||||
Flex::row()
|
||||
.with_child(Shrinkable::new(1., header.finish()).finish())
|
||||
.finish(),
|
||||
);
|
||||
} else {
|
||||
content.add_child(title);
|
||||
}
|
||||
|
||||
if let Some(output) = output {
|
||||
let body = render_requested_action_body_text(
|
||||
output.into(),
|
||||
appearance.monospace_font_family(),
|
||||
app,
|
||||
)
|
||||
.finish();
|
||||
let is_streaming = matches!(
|
||||
activity.status,
|
||||
Some(RuntimeActivityStatus::Pending | RuntimeActivityStatus::InProgress)
|
||||
);
|
||||
if let Some(scrollable) = render_scrollable_collapsible_content(
|
||||
&output_message.id,
|
||||
state,
|
||||
body,
|
||||
is_streaming,
|
||||
320.,
|
||||
) {
|
||||
content.add_child(Container::new(scrollable).with_margin_top(12.).finish());
|
||||
}
|
||||
}
|
||||
|
||||
let icon = match activity.status.as_ref() {
|
||||
Some(RuntimeActivityStatus::Completed) => {
|
||||
inline_action_icons::green_check_icon(appearance).finish()
|
||||
}
|
||||
Some(RuntimeActivityStatus::Failed) => inline_action_icons::red_x_icon(appearance).finish(),
|
||||
Some(RuntimeActivityStatus::Pending)
|
||||
| Some(RuntimeActivityStatus::InProgress)
|
||||
| Some(RuntimeActivityStatus::Other(_))
|
||||
| None => galaxyui::elements::Icon::new(
|
||||
Icon::ClockRefresh.into(),
|
||||
internal_colors::neutral_5(appearance.theme()),
|
||||
)
|
||||
.finish(),
|
||||
};
|
||||
|
||||
Some(
|
||||
RenderableAction::new_with_element(content.finish(), app)
|
||||
.with_icon(icon)
|
||||
.render(app)
|
||||
.finish(),
|
||||
)
|
||||
}
|
||||
|
||||
fn should_render_stopped_output(props: Props, app: &AppContext) -> bool {
|
||||
if FeatureFlag::AgentView.is_enabled() {
|
||||
return false;
|
||||
|
||||
@@ -103,6 +103,54 @@ fn collapsed_initializer_starts_collapsed() {
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn completed_runtime_activity_stays_collapsed_until_opened() {
|
||||
let mut state = CollapsibleElementState::collapsed();
|
||||
|
||||
state.sync_runtime_activity(false, true, true);
|
||||
|
||||
assert!(matches!(
|
||||
state.expansion_state,
|
||||
CollapsibleExpansionState::Collapsed
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn streaming_runtime_activity_expands_when_output_arrives() {
|
||||
let mut state = CollapsibleElementState::collapsed();
|
||||
|
||||
state.sync_runtime_activity(true, false, true);
|
||||
assert!(matches!(
|
||||
state.expansion_state,
|
||||
CollapsibleExpansionState::Expanded {
|
||||
is_finished: false,
|
||||
scroll_pinned_to_bottom: true
|
||||
}
|
||||
));
|
||||
|
||||
state.sync_runtime_activity(false, true, true);
|
||||
assert!(matches!(
|
||||
state.expansion_state,
|
||||
CollapsibleExpansionState::Expanded {
|
||||
is_finished: true,
|
||||
scroll_pinned_to_bottom: false
|
||||
}
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn manually_collapsed_streaming_runtime_activity_stays_collapsed() {
|
||||
let mut state = CollapsibleElementState::default();
|
||||
state.toggle_expansion();
|
||||
|
||||
state.sync_runtime_activity(true, false, true);
|
||||
|
||||
assert!(matches!(
|
||||
state.expansion_state,
|
||||
CollapsibleExpansionState::Collapsed
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn orchestration_show_and_collapse_collapses_after_finish() {
|
||||
let mut state = default_collapsible_state_for_orchestration_message(
|
||||
|
||||
@@ -754,6 +754,10 @@ impl BlocklistAIController {
|
||||
} => (conversation_id, task_id),
|
||||
};
|
||||
|
||||
BlocklistAIHistoryModel::handle(ctx).update(ctx, |history, ctx| {
|
||||
history.refresh_conversation_backend_without_output(conversation_id, ctx);
|
||||
});
|
||||
|
||||
let active_conversation_id =
|
||||
BlocklistAIHistoryModel::as_ref(ctx).active_conversation_id(self.terminal_surface_id);
|
||||
let is_same_conversation_running_command_monitor = match &input_query.input_query {
|
||||
|
||||
@@ -12,6 +12,8 @@ use anyhow::anyhow;
|
||||
use chrono::{DateTime, Local, TimeDelta};
|
||||
use futures::channel::oneshot;
|
||||
#[cfg(not(target_family = "wasm"))]
|
||||
use galaxy_agent_core::TurnCommand;
|
||||
#[cfg(not(target_family = "wasm"))]
|
||||
use galaxy_core::features::FeatureFlag;
|
||||
use galaxyui::{Entity, ModelContext, SingletonEntity};
|
||||
use settings::Setting;
|
||||
@@ -22,7 +24,7 @@ use warp_multi_agent_api::response_event;
|
||||
use crate::ai::acp::{
|
||||
acp_output_stream, acp_startup_error_stream, galaxy_mcp_server, resolve_acp_launch,
|
||||
resolve_acp_permissions, validate_acp_dispatch, validate_acp_launch_identity, AcpRuntimeModel,
|
||||
AcpSessionHandleSlot, AcpSessionMetadata, AcpSteeringRequest, GalaxyMcpTarget,
|
||||
AcpSessionMetadata, AcpTurnControlSlot, GalaxyMcpTarget,
|
||||
};
|
||||
use crate::ai::agent::api::{self, ConvertToAPITypeError};
|
||||
use crate::ai::agent::conversation::AIConversationId;
|
||||
@@ -108,8 +110,7 @@ impl ResponseStreamId {
|
||||
struct AcpRequestControl {
|
||||
cancellation_rx: oneshot::Receiver<()>,
|
||||
session_metadata: Arc<Mutex<AcpSessionMetadata>>,
|
||||
session_handle: AcpSessionHandleSlot,
|
||||
steering_rx: async_channel::Receiver<AcpSteeringRequest>,
|
||||
turn_control: AcpTurnControlSlot,
|
||||
}
|
||||
|
||||
/// Model wrapping an agent API response stream.
|
||||
@@ -125,9 +126,7 @@ pub struct ResponseStream {
|
||||
#[cfg(not(target_family = "wasm"))]
|
||||
acp_session_metadata: Arc<Mutex<AcpSessionMetadata>>,
|
||||
#[cfg(not(target_family = "wasm"))]
|
||||
acp_session_handle: AcpSessionHandleSlot,
|
||||
#[cfg(not(target_family = "wasm"))]
|
||||
acp_steering_tx: async_channel::Sender<AcpSteeringRequest>,
|
||||
acp_turn_control: AcpTurnControlSlot,
|
||||
params: api::RequestParams,
|
||||
retry_count: usize,
|
||||
/// One-time fallback from the profile's thinking model to its coding model.
|
||||
@@ -198,9 +197,7 @@ impl ResponseStream {
|
||||
#[cfg(not(target_family = "wasm"))]
|
||||
acp_session_metadata: Arc::new(Mutex::new(AcpSessionMetadata::default())),
|
||||
#[cfg(not(target_family = "wasm"))]
|
||||
acp_session_handle: Arc::new(Mutex::new(None)),
|
||||
#[cfg(not(target_family = "wasm"))]
|
||||
acp_steering_tx: async_channel::unbounded().0,
|
||||
acp_turn_control: Arc::new(Mutex::new(None)),
|
||||
params: api::RequestParams::new_for_test(),
|
||||
retry_count: 0,
|
||||
coding_model_fallback_attempted: false,
|
||||
@@ -328,8 +325,7 @@ impl ResponseStream {
|
||||
let AcpRequestControl {
|
||||
cancellation_rx,
|
||||
session_metadata,
|
||||
session_handle,
|
||||
steering_rx,
|
||||
turn_control,
|
||||
} = control;
|
||||
let profile = BlocklistAIPermissions::as_ref(ctx)
|
||||
.active_permissions_profile(ctx, params.terminal_view_id);
|
||||
@@ -398,8 +394,7 @@ impl ResponseStream {
|
||||
permissions.policy,
|
||||
permissions.auto_approve_protocol_requests,
|
||||
session_metadata,
|
||||
session_handle,
|
||||
steering_rx,
|
||||
turn_control,
|
||||
cancellation_rx,
|
||||
)
|
||||
.await
|
||||
@@ -447,9 +442,7 @@ impl ResponseStream {
|
||||
#[cfg(not(target_family = "wasm"))]
|
||||
let acp_session_metadata = Arc::new(Mutex::new(AcpSessionMetadata::default()));
|
||||
#[cfg(not(target_family = "wasm"))]
|
||||
let acp_session_handle = Arc::new(Mutex::new(None));
|
||||
#[cfg(not(target_family = "wasm"))]
|
||||
let (acp_steering_tx, acp_steering_rx) = async_channel::unbounded();
|
||||
let acp_turn_control = Arc::new(Mutex::new(None));
|
||||
match &agent_backend {
|
||||
AgentBackend::Provider => {
|
||||
let provider_config = Self::resolve_provider_config(params.model.as_str(), ctx);
|
||||
@@ -474,8 +467,7 @@ impl ResponseStream {
|
||||
AcpRequestControl {
|
||||
cancellation_rx,
|
||||
session_metadata: acp_session_metadata.clone(),
|
||||
session_handle: acp_session_handle.clone(),
|
||||
steering_rx: acp_steering_rx,
|
||||
turn_control: acp_turn_control.clone(),
|
||||
},
|
||||
ctx,
|
||||
);
|
||||
@@ -501,9 +493,7 @@ impl ResponseStream {
|
||||
#[cfg(not(target_family = "wasm"))]
|
||||
acp_session_metadata,
|
||||
#[cfg(not(target_family = "wasm"))]
|
||||
acp_session_handle,
|
||||
#[cfg(not(target_family = "wasm"))]
|
||||
acp_steering_tx,
|
||||
acp_turn_control,
|
||||
params: params.clone(),
|
||||
start_time,
|
||||
time_to_latest_event: TimeDelta::seconds(0),
|
||||
@@ -550,18 +540,23 @@ impl ResponseStream {
|
||||
|| !self
|
||||
.acp_session_metadata()
|
||||
.is_some_and(|metadata| metadata.can_steer)
|
||||
|| !self
|
||||
.acp_session_handle
|
||||
.lock()
|
||||
.is_ok_and(|session| session.is_some())
|
||||
{
|
||||
return false;
|
||||
}
|
||||
let mut model_text = display_text.clone();
|
||||
self.params.redact_text_for_model(&mut model_text);
|
||||
self.acp_steering_tx
|
||||
.try_send(AcpSteeringRequest::text(display_text, model_text))
|
||||
.is_ok()
|
||||
self.acp_turn_control
|
||||
.lock()
|
||||
.ok()
|
||||
.and_then(|control| control.clone())
|
||||
.is_some_and(|control| {
|
||||
control
|
||||
.try_send(TurnCommand::Steer {
|
||||
display_text,
|
||||
model_text,
|
||||
})
|
||||
.is_ok()
|
||||
})
|
||||
}
|
||||
#[cfg(target_family = "wasm")]
|
||||
{
|
||||
|
||||
@@ -1182,6 +1182,86 @@ impl BlocklistAIHistoryModel {
|
||||
});
|
||||
}
|
||||
|
||||
fn configured_agent_backend(
|
||||
is_viewing_shared_session: bool,
|
||||
is_cli_agent_transcript: bool,
|
||||
ctx: &AppContext,
|
||||
) -> AgentBackend {
|
||||
if is_viewing_shared_session
|
||||
|| is_cli_agent_transcript
|
||||
|| !cfg!(unix)
|
||||
|| !FeatureFlag::AgentClientProtocol.is_enabled()
|
||||
{
|
||||
return AgentBackend::Provider;
|
||||
}
|
||||
|
||||
let settings = AISettings::as_ref(ctx);
|
||||
if !*settings.acp_enabled.value() {
|
||||
return AgentBackend::Provider;
|
||||
}
|
||||
|
||||
let configured_agent_id = settings.acp_agent_id.value().trim();
|
||||
let agent_id = if configured_agent_id.is_empty() {
|
||||
"codex"
|
||||
} else {
|
||||
configured_agent_id
|
||||
};
|
||||
#[cfg(not(target_family = "wasm"))]
|
||||
let launch_fingerprint = acp_launch_fingerprint(
|
||||
agent_id,
|
||||
settings.acp_agent_command.value(),
|
||||
settings.acp_agent_args.value(),
|
||||
);
|
||||
#[cfg(target_family = "wasm")]
|
||||
let launch_fingerprint = String::new();
|
||||
AgentBackend::Acp(AcpConversationData {
|
||||
agent_id: agent_id.to_string(),
|
||||
launch_fingerprint,
|
||||
session_id: None,
|
||||
config_values: settings
|
||||
.acp_agents
|
||||
.value()
|
||||
.iter()
|
||||
.find(|agent| agent.id.eq_ignore_ascii_case(agent_id))
|
||||
.map(|agent| {
|
||||
#[cfg(not(target_family = "wasm"))]
|
||||
if let Some(selection) =
|
||||
LLMPreferences::as_ref(ctx).selected_acp_config_for_agent(&agent.name, ctx)
|
||||
{
|
||||
return selection;
|
||||
}
|
||||
crate::ai::acp::AcpRuntimeModel::current_config_values(&agent.config_options)
|
||||
})
|
||||
.unwrap_or_default(),
|
||||
})
|
||||
}
|
||||
|
||||
/// Reconciles a conversation without agent output with the currently enabled local runtime.
|
||||
///
|
||||
/// Agent views can create their initial conversation before the user changes runtime settings,
|
||||
/// and a provider-less attempt can leave behind an error-only exchange. Refreshing here lets
|
||||
/// either case use ACP without mixing successful provider output into an ACP-owned history.
|
||||
pub(crate) fn refresh_conversation_backend_without_output(
|
||||
&mut self,
|
||||
conversation_id: AIConversationId,
|
||||
ctx: &AppContext,
|
||||
) {
|
||||
let Some(conversation) = self.conversation(&conversation_id) else {
|
||||
return;
|
||||
};
|
||||
let agent_backend = Self::configured_agent_backend(
|
||||
conversation.is_viewing_shared_session(),
|
||||
conversation.is_cli_agent_transcript(),
|
||||
ctx,
|
||||
);
|
||||
if conversation.agent_backend() == &agent_backend {
|
||||
return;
|
||||
}
|
||||
if let Some(conversation) = self.conversation_mut(&conversation_id) {
|
||||
conversation.set_agent_backend_if_no_output(agent_backend);
|
||||
}
|
||||
}
|
||||
|
||||
/// Starts a new conversation in the given terminal surface's history, effectively marking the
|
||||
/// existing conversation (if any) as completed.
|
||||
///
|
||||
@@ -1197,55 +1277,8 @@ impl BlocklistAIHistoryModel {
|
||||
is_cli_agent_transcript: bool,
|
||||
ctx: &mut ModelContext<Self>,
|
||||
) -> AIConversationId {
|
||||
let agent_backend = if !is_viewing_shared_session
|
||||
&& !is_cli_agent_transcript
|
||||
&& cfg!(unix)
|
||||
&& FeatureFlag::AgentClientProtocol.is_enabled()
|
||||
{
|
||||
let settings = AISettings::as_ref(ctx);
|
||||
if *settings.acp_enabled.value() {
|
||||
let configured_agent_id = settings.acp_agent_id.value().trim();
|
||||
let agent_id = if configured_agent_id.is_empty() {
|
||||
"codex"
|
||||
} else {
|
||||
configured_agent_id
|
||||
};
|
||||
#[cfg(not(target_family = "wasm"))]
|
||||
let launch_fingerprint = acp_launch_fingerprint(
|
||||
agent_id,
|
||||
settings.acp_agent_command.value(),
|
||||
settings.acp_agent_args.value(),
|
||||
);
|
||||
#[cfg(target_family = "wasm")]
|
||||
let launch_fingerprint = String::new();
|
||||
AgentBackend::Acp(AcpConversationData {
|
||||
agent_id: agent_id.to_string(),
|
||||
launch_fingerprint,
|
||||
session_id: None,
|
||||
config_values: settings
|
||||
.acp_agents
|
||||
.value()
|
||||
.iter()
|
||||
.find(|agent| agent.id.eq_ignore_ascii_case(agent_id))
|
||||
.map(|agent| {
|
||||
#[cfg(not(target_family = "wasm"))]
|
||||
if let Some(selection) = LLMPreferences::as_ref(ctx)
|
||||
.selected_acp_config_for_agent(&agent.name, ctx)
|
||||
{
|
||||
return selection;
|
||||
}
|
||||
crate::ai::acp::AcpRuntimeModel::current_config_values(
|
||||
&agent.config_options,
|
||||
)
|
||||
})
|
||||
.unwrap_or_default(),
|
||||
})
|
||||
} else {
|
||||
AgentBackend::Provider
|
||||
}
|
||||
} else {
|
||||
AgentBackend::Provider
|
||||
};
|
||||
let agent_backend =
|
||||
Self::configured_agent_backend(is_viewing_shared_session, is_cli_agent_transcript, ctx);
|
||||
let mut new_conversation = AIConversation::new_with_agent_backend(
|
||||
is_viewing_shared_session,
|
||||
is_cli_agent_transcript,
|
||||
|
||||
@@ -88,6 +88,82 @@ fn acp_enabled_with_empty_command_selects_codex_backend() {
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn enabling_acp_refreshes_a_provider_conversation_with_only_failed_output() {
|
||||
let _acp_flag = FeatureFlag::AgentClientProtocol.override_enabled(true);
|
||||
App::test((), |mut app| async move {
|
||||
initialize_history_persistence_for_tests(&mut app);
|
||||
AISettings::handle(&app).update(&mut app, |settings, ctx| {
|
||||
settings
|
||||
.acp_enabled
|
||||
.set_value(false, ctx)
|
||||
.expect("ACP setting should update");
|
||||
});
|
||||
|
||||
let terminal_view_id = EntityId::new();
|
||||
let history_model = app.add_singleton_model(|_| BlocklistAIHistoryModel::new_for_test());
|
||||
let conversation_id = history_model.update(&mut app, |model, ctx| {
|
||||
model.start_new_conversation(terminal_view_id, false, false, false, ctx)
|
||||
});
|
||||
history_model.read(&app, |model, _| {
|
||||
assert_eq!(
|
||||
model
|
||||
.conversation(&conversation_id)
|
||||
.expect("conversation should exist")
|
||||
.agent_backend(),
|
||||
&AgentBackend::Provider
|
||||
);
|
||||
});
|
||||
history_model.update(&mut app, |model, _| {
|
||||
let now = Local::now();
|
||||
model
|
||||
.conversation_mut(&conversation_id)
|
||||
.expect("conversation should exist")
|
||||
.append_root_exchange_for_test(AIAgentExchange {
|
||||
id: AIAgentExchangeId::new(),
|
||||
input: Vec::new(),
|
||||
output_status: AIAgentOutputStatus::Finished {
|
||||
finished_output: FinishedAIAgentOutput::Error {
|
||||
output: None,
|
||||
error: RenderableAIError::other("No AI provider configured", true),
|
||||
},
|
||||
},
|
||||
added_message_ids: HashSet::new(),
|
||||
start_time: now,
|
||||
finish_time: Some(now),
|
||||
time_to_first_token_ms: None,
|
||||
working_directory: None,
|
||||
model_id: LLMId::from("none"),
|
||||
request_cost: None,
|
||||
coding_model_id: LLMId::from("none"),
|
||||
cli_agent_model_id: LLMId::from("none"),
|
||||
computer_use_model_id: LLMId::from("none"),
|
||||
response_initiator: None,
|
||||
});
|
||||
});
|
||||
|
||||
AISettings::handle(&app).update(&mut app, |settings, ctx| {
|
||||
settings
|
||||
.acp_enabled
|
||||
.set_value(true, ctx)
|
||||
.expect("ACP setting should update");
|
||||
});
|
||||
history_model.update(&mut app, |model, ctx| {
|
||||
model.refresh_conversation_backend_without_output(conversation_id, ctx);
|
||||
});
|
||||
|
||||
history_model.read(&app, |model, _| {
|
||||
assert!(matches!(
|
||||
model
|
||||
.conversation(&conversation_id)
|
||||
.expect("conversation should exist")
|
||||
.agent_backend(),
|
||||
AgentBackend::Acp(_)
|
||||
));
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/// Helper function to create a PersistedAIInput for testing
|
||||
fn create_persisted_query(
|
||||
query_text: &str,
|
||||
|
||||
@@ -469,6 +469,7 @@ impl OrchestrationEventService {
|
||||
| AIAgentOutputMessageType::Reasoning { .. }
|
||||
| AIAgentOutputMessageType::Summarization { .. }
|
||||
| AIAgentOutputMessageType::Subagent(_)
|
||||
| AIAgentOutputMessageType::RuntimeActivity(_)
|
||||
| AIAgentOutputMessageType::Action(_)
|
||||
| AIAgentOutputMessageType::TodoOperation(_)
|
||||
| AIAgentOutputMessageType::WebSearch(_)
|
||||
|
||||
Reference in New Issue
Block a user