use crate::app::agent::agent::loop_::progress;
use crate::app::agent::config::Config;
use crate::app::agent::memory::MemoryCategory;
use crate::session::ui_types as models;
use anyhow::Result;
use super::super::super::processor::run_session_processor_for_cli;
use super::super::super::session::{collect_modified_files, maybe_refresh_cli_session_title};
use super::super::super::setup::CliSetup;
use super::super::super::stats::{CliStats, build_session_title};
use super::super::super::transcript::{
TranscriptEntry, TranscriptRole, build_streaming_transcript_view,
};
use super::super::super::tui::CliTui;
use super::commands::{handle_inline_command, handle_pending_clear};
use crate::app::agent::agent::loop_::context::build_context;
use crate::app::agent::agent::loop_::core::{
AUTOSAVE_MIN_MESSAGE_CHARS, autosave_memory_key, effective_message_timeout_secs,
is_tool_iteration_limit_error, message_timeout_budget_secs,
};
use crossterm::event::{self, Event as CrosstermEvent, KeyCode, KeyEventKind};
#[allow(clippy::too_many_arguments)]
pub(crate) async fn handle_submit_result(
config: &Config,
setup: &CliSetup,
user_input: String,
tui: &mut CliTui,
transcript: &mut Vec<TranscriptEntry>,
session_history: &mut Vec<models::ChatMessage>,
session_id: &mut String,
stream_id: &mut u64,
session_title_refreshed: &mut bool,
input: &str,
cursor_idx: &mut usize,
busy: &mut bool,
awaiting_clear_confirm: &mut bool,
stats: &mut CliStats,
workspace: &str,
modified_files: &mut Vec<String>,
files_collapsed: &mut bool,
draft: &mut String,
scroll_back: &mut u16,
show_menu: &mut bool,
final_output: &mut String,
) -> Result<SubmitOutcome> {
const ESC_INTERRUPT_ERROR: &str = "__cli_turn_interrupted_by_esc__";
if user_input.is_empty() {
let session_title = build_session_title(stats, &setup.provider_name, &setup.model_name);
tui.draw(
transcript,
input,
*cursor_idx,
*busy,
*awaiting_clear_confirm,
&setup.provider_name,
&setup.model_name,
stats,
workspace,
draft,
&session_title,
modified_files,
*files_collapsed,
*scroll_back,
*show_menu,
)?;
return Ok(SubmitOutcome::Continue);
}
transcript.push(TranscriptEntry::new(TranscriptRole::User, user_input.clone()));
stats.user_messages += 1;
*scroll_back = 0;
if *awaiting_clear_confirm {
return handle_pending_clear(
config,
setup,
&user_input,
tui,
transcript,
session_history,
session_id,
session_title_refreshed,
*cursor_idx,
*busy,
awaiting_clear_confirm,
stats,
workspace,
modified_files,
files_collapsed,
draft,
*scroll_back,
*show_menu,
)
.await;
}
if let Some(outcome) = handle_inline_command(
&user_input,
tui,
transcript,
*cursor_idx,
*busy,
awaiting_clear_confirm,
stats,
workspace,
modified_files,
files_collapsed,
draft,
*scroll_back,
*show_menu,
&setup.provider_name,
&setup.model_name,
)? {
return Ok(outcome);
}
if config.memory.auto_save && user_input.chars().count() >= AUTOSAVE_MIN_MESSAGE_CHARS {
let user_key = autosave_memory_key("user_msg");
let _ = setup.mem.store(&user_key, &user_input, MemoryCategory::Conversation, None).await;
}
let mem_context =
build_context(setup.mem.as_ref(), &user_input, config.memory.min_relevance_score).await;
let enriched = if mem_context.is_empty() {
user_input.clone()
} else {
format!("{mem_context}{user_input}")
};
*busy = true;
draft.clear();
let session_title = build_session_title(stats, &setup.provider_name, &setup.model_name);
tui.draw(
transcript,
input,
*cursor_idx,
*busy,
*awaiting_clear_confirm,
&setup.provider_name,
&setup.model_name,
stats,
workspace,
draft,
&session_title,
modified_files,
*files_collapsed,
*scroll_back,
*show_menu,
)?;
let response_result = {
let (tx, mut rx) = tokio::sync::mpsc::channel::<String>(64);
*stream_id = stream_id.saturating_add(1);
let message_timeout_secs =
effective_message_timeout_secs(config.channels_config.message_timeout_secs);
let timeout_budget_secs =
message_timeout_budget_secs(message_timeout_secs, config.agent.max_tool_iterations);
let req = crate::app::agent::session::processor::Request {
stream: *stream_id,
session: session_id.clone(),
query: enriched.clone(),
root: Some(
std::env::current_dir()
.unwrap_or_else(|_| config.workspace_dir.clone())
.to_string_lossy()
.to_string(),
),
model: Some(setup.model_name.to_string()),
options: serde_json::Value::Object(serde_json::Map::new()),
approval: None,
channel_name: None,
non_cli_approval_context: None,
assistant_message_id: None,
history: session_history.clone(),
persist_app_session_artifacts: true,
};
let mut turn_future = std::pin::pin!(tokio::time::timeout(
std::time::Duration::from_secs(timeout_budget_secs),
run_session_processor_for_cli(req, Some(tx)),
));
loop {
tokio::select! {
result = &mut turn_future => {
break result;
}
() = tokio::time::sleep(std::time::Duration::from_millis(80)) => {
tui.tick();
let (display_transcript, display_draft) =
build_streaming_transcript_view(transcript, draft, tui.expand_tool_blocks);
let session_title = build_session_title(stats, &setup.provider_name, &setup.model_name);
tui.draw(
&display_transcript,
input,
*cursor_idx,
*busy,
*awaiting_clear_confirm,
&setup.provider_name,
&setup.model_name,
stats,
workspace,
&display_draft,
&session_title,
modified_files,
*files_collapsed,
*scroll_back,
*show_menu,
)?;
if consume_escape_keypress()? {
break Ok(Err(anyhow::anyhow!(ESC_INTERRUPT_ERROR)));
}
}
maybe_msg = rx.recv() => {
if let Some(msg) = maybe_msg {
if msg == progress::DRAFT_CLEAR_SENTINEL {
draft.clear();
} else if let Some(stripped) =
msg.strip_prefix(progress::DRAFT_PROGRESS_SENTINEL)
{
draft.push_str(stripped);
} else {
draft.push_str(&msg);
}
let (display_transcript, display_draft) =
build_streaming_transcript_view(transcript, draft, tui.expand_tool_blocks);
let session_title = build_session_title(stats, &setup.provider_name, &setup.model_name);
tui.draw(
&display_transcript,
input,
*cursor_idx,
*busy,
*awaiting_clear_confirm,
&setup.provider_name,
&setup.model_name,
stats,
workspace,
&display_draft,
&session_title,
modified_files,
*files_collapsed,
*scroll_back,
*show_menu,
)?;
}
}
}
}
};
let response = match response_result {
Ok(Ok(resp)) => resp,
Err(_) => {
*busy = false;
*modified_files = collect_modified_files(&config.workspace_dir);
transcript.push(TranscriptEntry::new(
TranscriptRole::System,
"⚠️ 请求超时,等待模型响应超时。请稍后重试。",
));
let session_title = build_session_title(stats, &setup.provider_name, &setup.model_name);
tui.draw(
transcript,
input,
*cursor_idx,
*busy,
*awaiting_clear_confirm,
&setup.provider_name,
&setup.model_name,
stats,
workspace,
draft,
&session_title,
modified_files,
*files_collapsed,
*scroll_back,
*show_menu,
)?;
return Ok(SubmitOutcome::Continue);
}
Ok(Err(e)) => {
*busy = false;
if e.to_string() == ESC_INTERRUPT_ERROR {
draft.clear();
transcript.push(TranscriptEntry::new(TranscriptRole::System, "已中断当前对话"));
let session_title =
build_session_title(stats, &setup.provider_name, &setup.model_name);
tui.draw(
transcript,
input,
*cursor_idx,
*busy,
*awaiting_clear_confirm,
&setup.provider_name,
&setup.model_name,
stats,
workspace,
draft,
&session_title,
modified_files,
*files_collapsed,
*scroll_back,
*show_menu,
)?;
return Ok(SubmitOutcome::Continue);
}
*modified_files = collect_modified_files(&config.workspace_dir);
if is_tool_iteration_limit_error(&e) {
let limit = config.agent.max_tool_iterations.max(1);
let pause_notice = format!(
"⚠️ 已达到工具迭代次数限制 ({limit}),上下文和进度已保留。回复 \"continue\" 继续,或增加 `agent.max_tool_iterations` 配置。"
);
transcript.push(TranscriptEntry::new(TranscriptRole::System, pause_notice));
let session_title =
build_session_title(stats, &setup.provider_name, &setup.model_name);
tui.draw(
transcript,
input,
*cursor_idx,
*busy,
*awaiting_clear_confirm,
&setup.provider_name,
&setup.model_name,
stats,
workspace,
draft,
&session_title,
modified_files,
*files_collapsed,
*scroll_back,
*show_menu,
)?;
return Ok(SubmitOutcome::Continue);
}
transcript.push(TranscriptEntry::new(TranscriptRole::Error, e.to_string()));
let session_title = build_session_title(stats, &setup.provider_name, &setup.model_name);
tui.draw(
transcript,
input,
*cursor_idx,
*busy,
*awaiting_clear_confirm,
&setup.provider_name,
&setup.model_name,
stats,
workspace,
draft,
&session_title,
modified_files,
*files_collapsed,
*scroll_back,
*show_menu,
)?;
return Ok(SubmitOutcome::Continue);
}
};
*busy = false;
draft.clear();
*final_output = response.output.clone();
transcript.push(TranscriptEntry::new(TranscriptRole::Assistant, response.output.clone()));
session_history.push(models::ChatMessage {
role: models::ChatRole::User,
content: enriched,
think_timing: Vec::new(),
});
session_history.push(models::ChatMessage {
role: models::ChatRole::Assistant,
content: response.output,
think_timing: Vec::new(),
});
stats.assistant_messages += 1;
stats.input_tokens =
stats.input_tokens.saturating_add(response.usage.input_tokens.max(0).cast_unsigned());
stats.output_tokens =
stats.output_tokens.saturating_add(response.usage.output_tokens.max(0).cast_unsigned());
*modified_files = collect_modified_files(&config.workspace_dir);
*scroll_back = 0;
stats.tool_events = stats.tool_events.saturating_add(response.step_finishes);
if !*session_title_refreshed {
maybe_refresh_cli_session_title(
session_id,
&user_input,
Some(setup.model_name.to_string()),
)
.await;
*session_title_refreshed = true;
}
setup.observer.record_event(&crate::app::agent::observability::ObserverEvent::TurnComplete);
Ok(SubmitOutcome::Continue)
}
fn consume_escape_keypress() -> Result<bool> {
if !event::poll(std::time::Duration::from_millis(0))? {
return Ok(false);
}
let evt = event::read()?;
let CrosstermEvent::Key(key) = evt else {
return Ok(false);
};
Ok(key.kind == KeyEventKind::Press && key.code == KeyCode::Esc)
}
#[derive(PartialEq, Eq)]
pub(crate) enum SubmitOutcome {
Continue,
Exit,
}