Auto-monitor long-running commands with agent

This commit is contained in:
Ryan Ward
2026-07-28 17:11:34 -05:00
parent b75aa00fb0
commit 5c14241b91
4 changed files with 98 additions and 16 deletions
@@ -413,6 +413,10 @@ impl AgentViewController {
self.agent_view_state.is_active()
}
pub fn active_conversation_id(&self) -> Option<AIConversationId> {
self.agent_view_state.active_conversation_id()
}
pub fn is_inline(&self) -> bool {
self.agent_view_state.is_inline()
}
@@ -93,6 +93,8 @@ pub struct ResponseStream {
id: ResponseStreamId,
params: api::RequestParams,
retry_count: usize,
/// One-time fallback from the profile's thinking model to its coding model.
coding_model_fallback_attempted: bool,
start_time: DateTime<Local>,
time_to_latest_event: TimeDelta,
cancellation_tx: Option<oneshot::Sender<()>>,
@@ -254,6 +256,7 @@ impl ResponseStream {
time_to_latest_event: TimeDelta::seconds(0),
cancellation_tx: Some(cancellation_tx),
retry_count: 0,
coding_model_fallback_attempted: false,
original_error: None,
has_received_client_actions: false,
ai_identifiers,
@@ -320,7 +323,7 @@ impl ResponseStream {
let request_id = Uuid::new_v4();
self.current_request_id = Some(request_id);
let params = self.params.clone();
let mut params = self.params.clone();
let provider_config = Self::resolve_provider_config(params.model.as_str(), ctx);
let server_api = ServerApiProvider::as_ref(ctx).get_ai_client().clone();
let _ = ctx.spawn(
@@ -334,6 +337,28 @@ impl ResponseStream {
);
}
fn should_fallback_to_coding_model(
&self,
error: &Arc<crate::server::server_api::AIApiError>,
) -> bool {
if self.coding_model_fallback_attempted || self.has_received_client_actions {
return false;
}
let coding_model = self.params.coding_model.as_str();
!coding_model.is_empty()
&& coding_model != self.params.model.as_str()
&& matches!(
error.as_ref(),
crate::server::server_api::AIApiError::QuotaLimit { .. }
)
}
fn retry_with_coding_model(&mut self, ctx: &mut ModelContext<Self>) {
self.coding_model_fallback_attempted = true;
self.params.model = self.params.coding_model.clone();
self.retry(ctx);
}
/// Cancels the stream. The conversation_id is preserved in the emitted event for async handling.
pub(super) fn cancel(
&mut self,
@@ -478,6 +503,14 @@ impl ResponseStream {
self.original_error = Some(format!("{e:?}"));
}
if self.should_fallback_to_coding_model(&e) {
log::warn!(
"Thinking model rate-limited; retrying with the profile coding model"
);
self.retry_with_coding_model(ctx);
return;
}
let is_online = NetworkStatus::as_ref(ctx).is_online();
match recovery_action(
self.has_received_client_actions,
+1 -1
View File
@@ -67,7 +67,7 @@ use crate::terminal::shell::ShellType;
use crate::terminal::view::WithinBlockBanner;
use crate::terminal::{BlockPadding, ShellHost, SizeInfo};
pub const LONG_RUNNING_COMMAND_DURATION_MS: u64 = 50;
pub const LONG_RUNNING_COMMAND_DURATION_MS: u64 = 3_000;
pub const LONG_RUNNING_BOTTOM_PADDING_LINES: f32 = 0.2;
/// We don't consider commands that were killed via Ctrl-C (error code 130) or that were killed
+59 -14
View File
@@ -7482,7 +7482,7 @@ impl TerminalView {
drop(model);
ctx.emit(Event::ExecuteCommand(ExecuteCommandEvent {
command,
command: command.clone(),
session_id,
source,
should_add_command_to_history: true,
@@ -7498,21 +7498,66 @@ impl TerminalView {
});
}
// If the command turns out to be long-running, lock the input in agent mode.
// After three seconds, automatically open the inline command-monitoring agent.
// Use the same established tag-in path as the manual "Use agent" affordance so
// running-command context, the CLI subagent task, and main-conversation history
// remain connected through the existing machinery.
ctx.spawn(
// Command execution is triggered by a subscriber to the event above, so
// give some buffer to actually determine if its long running.
Timer::after(Duration::from_millis(LONG_RUNNING_COMMAND_DURATION_MS * 2)),
Timer::after(Duration::from_millis(LONG_RUNNING_COMMAND_DURATION_MS)),
move |me, _, ctx| {
if me
.model
.lock()
.block_list()
.block_with_id(&block_id)
.is_some_and(|block| block.is_active_and_long_running())
{
me.input.update(ctx, |input, ctx| {
input.set_input_mode_agent(false, ctx);
let is_still_running = {
let model = me.model.lock();
model
.block_list()
.block_with_id(&block_id)
.is_some_and(|block| block.is_active_and_long_running())
};
if !is_still_running {
return;
}
me.agent_view_controller.update(ctx, |controller, ctx| {
if !controller.is_active() {
if let Err(error) = controller.try_enter_inline_agent_view(
None,
AgentViewEntryOrigin::LongRunningCommand,
ctx,
) {
log::error!(
"Failed to automatically open long-running command monitor: {error}"
);
}
}
});
me.tag_in_agent_for_user_long_running_command(ctx);
let active_profile = AIExecutionProfilesModel::as_ref(ctx)
.active_profile(Some(me.view_id), ctx);
let profile_name = active_profile.data().name.clone();
let coding_model = active_profile
.data()
.coding_model
.as_ref()
.map(|model| model.as_str())
.unwrap_or("profile default");
log::info!(
"Opening long-running command monitor with selected profile {profile_name:?} (coding model {coding_model})"
);
let prompt = format!(
"Monitor this running command and report evidence-based status. Use the currently selected execution profile ({profile_name}) and its configured model choices. Identify concrete success signals, explicit failures, repeated retries, blocked input, lock waits, and suspicious lack of progress. Do not declare success merely because output stops, and do not interrupt or modify the process. For database work, flag a small update that appears stuck and distinguish a likely lock wait or deadlock from legitimate work when possible.\n\nCommand:\n```sh\n{command}\n```"
);
let conversation_id = me
.agent_view_controller
.as_ref(ctx)
.active_conversation_id();
if let Some(conversation_id) = conversation_id {
me.ai_controller.update(ctx, |controller, ctx| {
controller.send_agent_query_in_conversation(
prompt,
conversation_id,
ctx,
);
});
}
},