Remove default agent turn cap and preserve queued tool status

Allow new provider runs to continue without an arbitrary lifetime turn budget while preserving explicit limits, checkpoint compatibility, and bounded retries. Keep pending tool proposals visibly queued until execution status arrives, and show cancellation only when supported by actual state.

Validation: formatting, inline test-layout checks, both presubmit Clippy commands, 325 targeted regressions, and 54 core tests after final cleanup passed.
This commit is contained in:
2026-09-11 05:45:19 -05:00
parent 2d581f4c01
commit 73d6839ee9
5 changed files with 113 additions and 33 deletions
@@ -6,6 +6,7 @@ use crate::ai::agent::{
AIAgentAction, AIAgentActionId, AIAgentActionType, AIAgentInput, AIAgentOutputMessageType,
SummarizationType,
};
use crate::ai::blocklist::action_model::AIActionStatus;
use crate::ai::blocklist::BlocklistAIActionModel;
use crate::BlocklistAIHistoryModel;
@@ -14,6 +15,12 @@ use crate::BlocklistAIHistoryModel;
// These are defined within a separate trait rather than default implementations of `AIBlockModel`
// so implementations cannot errantly override them.
pub trait AIBlockModelHelper {
fn action_status_for_display(
&self,
action_model: &BlocklistAIActionModel,
action_id: &AIAgentActionId,
app: &AppContext,
) -> Option<AIActionStatus>;
fn is_first_action_in_output(&self, action_id: &AIAgentActionId, app: &AppContext) -> bool;
fn conversation<'a>(&self, app: &'a AppContext) -> Option<&'a AIConversation>;
@@ -41,6 +48,26 @@ pub trait AIBlockModelHelper {
}
impl<T: ?Sized + AIBlockModel> AIBlockModelHelper for T {
fn action_status_for_display(
&self,
action_model: &BlocklistAIActionModel,
action_id: &AIAgentActionId,
app: &AppContext,
) -> Option<AIActionStatus> {
let conversation = self.conversation(app)?;
let status = action_model.get_action_status(conversation.id(), action_id);
// Proposals appear before the full batch is handed to the executor. A
// finished output message is not evidence that an unstarted tool was cancelled.
status.or_else(|| {
(!conversation.status().is_done()
&& self.is_latest_visible_exchange_in_root_task(app)
&& self.status(app).output_to_render().is_some_and(|output| {
output.get().actions().any(|action| action.id == *action_id)
}))
.then_some(AIActionStatus::Queued)
})
}
fn is_first_action_in_output(&self, action_id: &AIAgentActionId, app: &AppContext) -> bool {
self.status(app).output_to_render().is_some_and(|output| {
output
@@ -3702,17 +3702,11 @@ pub fn action_icon<V: View>(
app: &AppContext,
) -> galaxyui::elements::Icon {
let appearance = Appearance::as_ref(app);
let status = ai_block_model
.conversation_id(app)
.and_then(|conversation_id| {
action_model
.as_ref(app)
.get_action_status(conversation_id, action_id)
});
let status = ai_block_model.action_status_for_display(action_model.as_ref(app), action_id, app);
match status {
Some(status) => match status {
AIActionStatus::Preprocessing => icons::gray_circle_icon(appearance),
AIActionStatus::Queued => icons::gray_stop_icon(appearance),
AIActionStatus::Queued => icons::gray_clock_icon(appearance),
AIActionStatus::Blocked => icons::yellow_stop_icon(appearance),
AIActionStatus::RunningAsync => icons::yellow_running_icon(appearance),
AIActionStatus::Finished(result) => {
@@ -3741,8 +3735,13 @@ pub fn action_icon<V: View>(
} else {
icons::gray_circle_icon(appearance)
}
} else {
} else if ai_block_model
.conversation(app)
.is_some_and(|conversation| conversation.status().is_cancelled())
{
inline_action_icons::cancelled_icon(appearance)
} else {
icons::gray_circle_icon(appearance)
}
}
}
@@ -1171,10 +1171,11 @@ impl RequestedCommandView {
app: &AppContext,
) -> Box<dyn Element> {
let appearance = Appearance::as_ref(app);
let action_status = self
.action_model
.as_ref(app)
.get_action_status(self.client_ids.conversation_id, &self.action_id);
let action_status = self.block_model.action_status_for_display(
self.action_model.as_ref(app),
&self.action_id,
app,
);
let mut title: Cow<'static, str>;
let mut font_override = None;
@@ -1541,10 +1542,11 @@ impl View for RequestedCommandView {
fn render(&self, app: &AppContext) -> Box<dyn Element> {
let appearance = Appearance::as_ref(app);
let theme = appearance.theme();
let action_status = self
.action_model
.as_ref(app)
.get_action_status(self.client_ids.conversation_id, &self.action_id);
let action_status = self.block_model.action_status_for_display(
self.action_model.as_ref(app),
&self.action_id,
app,
);
let is_last_output_message_in_output = self
.block_model