use super::chat_app::ChatApp;
use super::system_prompt::{StaticPlaceholderValues, apply_static_placeholders};
use crate::command::chat::agent::config::{AgentLoopConfig, AgentLoopSharedState};
use crate::command::chat::infra::command;
use crate::command::chat::infra::hook::{HookContext, HookEvent, HookManager};
use crate::command::chat::infra::skill::{self, skills_dir};
use crate::command::chat::storage::{ChatMessage, MessageRole};
use crate::util::safe_lock;
use std::sync::Arc;
use tokio_util::sync::CancellationToken;
impl ChatApp {
pub fn send_message(&mut self) {
let text = self.ui.input_text().trim().to_string();
if text.is_empty() {
return;
}
self.ui.at_popup_active = false;
self.ui.file_popup_active = false;
self.ui.skill_popup_active = false;
self.ui.clear_input();
self.send_message_internal(text);
}
pub fn send_message_internal(&mut self, text: String) {
let hook_result = {
let has_hooks = self
.hook_manager
.lock()
.map(|m| m.has_hooks_for(HookEvent::PreSendMessage))
.unwrap_or(false);
if has_hooks {
let ctx = HookContext {
event: HookEvent::PreSendMessage,
user_input: Some(text.clone()),
messages: Some(self.state.session.messages.clone()),
session_id: Some(self.session_id.clone()),
cwd: std::env::current_dir()
.map(|p| p.display().to_string())
.unwrap_or_else(|_| ".".to_string()),
..Default::default()
};
if let Ok(manager) = self.hook_manager.lock() {
manager.execute(HookEvent::PreSendMessage, ctx)
} else {
None
}
} else {
None
}
};
let text = if let Some(result) = hook_result {
if result.is_stop() {
self.show_toast("消息发送被 hook 拦截", true);
return;
}
result.user_input.unwrap_or(text)
} else {
text
};
let text = command::expand_command_mentions(
&text,
&self.state.loaded_commands,
&self.state.agent_config.disabled_commands,
);
self.state
.session
.messages
.push(ChatMessage::text(MessageRole::User, &text));
self.ui.auto_scroll = true;
self.ui.scroll_offset = u16::MAX;
{
let has_hooks = self
.hook_manager
.lock()
.map(|m| m.has_hooks_for(HookEvent::PostSendMessage))
.unwrap_or(false);
if has_hooks {
let ctx = HookContext {
event: HookEvent::PostSendMessage,
user_input: Some(text.clone()),
messages: Some(self.state.session.messages.clone()),
session_id: Some(self.session_id.clone()),
cwd: std::env::current_dir()
.map(|p| p.display().to_string())
.unwrap_or_else(|_| ".".to_string()),
..Default::default()
};
HookManager::execute_fire_and_forget(
Arc::clone(&self.hook_manager),
HookEvent::PostSendMessage,
ctx,
);
}
}
let provider = match self.active_provider() {
Some(p) => p.clone(),
None => {
self.show_toast("未配置模型提供方,请先编辑配置文件", true);
return;
}
};
{
let mut p = safe_lock(&self.derived_agent_provider, "send_message::agent_provider");
*p = provider.clone();
}
self.state.is_loading = true;
self.ui.last_rendered_streaming_len = 0;
self.ui.last_stream_render_time = std::time::Instant::now();
self.ui.msg_lines_cache = None;
self.tool_executor.reset();
let api_messages = self.build_api_messages();
{
let mut pending = safe_lock(
&self.state.pending_user_messages,
"send_message::pending_user_messages",
);
pending.clear();
}
{
let mut sc = safe_lock(
&self.state.streaming_content,
"send_message::streaming_content",
);
sc.clear();
}
self.spawn_agent_loop(provider, api_messages);
}
pub fn wake_from_teammate_inbox(&mut self) {
{
let mut pending =
safe_lock(&self.state.pending_user_messages, "wake_from_inbox::clear");
if pending.is_empty() {
return;
}
pending.clear();
}
let provider = match self.active_provider() {
Some(p) => p.clone(),
None => {
self.show_toast("未配置模型提供方,请先编辑配置文件", true);
return;
}
};
{
let mut p = safe_lock(
&self.derived_agent_provider,
"wake_from_inbox::agent_provider",
);
*p = provider.clone();
}
self.state.is_loading = true;
self.ui.last_rendered_streaming_len = 0;
self.ui.last_stream_render_time = std::time::Instant::now();
self.ui.msg_lines_cache = None;
self.tool_executor.reset();
let api_messages = self.build_api_messages();
{
let mut sc = safe_lock(
&self.state.streaming_content,
"wake_from_inbox::streaming_content",
);
sc.clear();
}
self.spawn_agent_loop(provider, api_messages);
}
fn spawn_agent_loop(
&mut self,
provider: crate::command::chat::storage::ModelProvider,
api_messages: Vec<ChatMessage>,
) {
use super::agent_handle::MainAgentHandle;
let streaming_content = Arc::clone(&self.state.streaming_content);
let tools_enabled = self.state.agent_config.tools_enabled;
let max_llm_rounds = self.state.agent_config.max_tool_rounds;
let tools = if tools_enabled {
self.tool_registry
.to_openai_tools_filtered(&self.state.agent_config.disabled_tools)
} else {
vec![]
};
let pending_user_messages = Arc::clone(&self.state.pending_user_messages);
let background_manager = Arc::clone(&self.background_manager);
let compact_config = self.state.agent_config.compact.clone();
let loaded_skills = self.state.loaded_skills.clone();
let disabled_skills = self.state.agent_config.disabled_skills.clone();
let disabled_tools = self.state.agent_config.disabled_tools.clone();
let tool_registry = Arc::clone(&self.tool_registry);
let system_prompt_fn: Arc<dyn Fn() -> Option<String> + Send + Sync> = Arc::new(move || {
use crate::command::chat::agent_md;
use crate::command::chat::storage::{
load_memory, load_soul, load_style, load_system_prompt,
};
let template = load_system_prompt()?;
let skills_summary = skill::build_skills_summary(&loaded_skills, &disabled_skills);
let tools_summary = tool_registry.build_tools_summary(&disabled_tools);
let style_text = load_style().unwrap_or_else(|| "(未设置)".to_string());
let memory_text = load_memory().unwrap_or_default();
let soul_text = load_soul().unwrap_or_default();
let agent_md_text = agent_md::load_agent_md();
let current_dir = std::env::current_dir()
.map(|p| p.display().to_string())
.unwrap_or_else(|_| ".".to_string());
let skill_dir = skills_dir().to_string_lossy().to_string();
let project_skill_dir = skill::project_skills_dir()
.map(|p| p.to_string_lossy().to_string())
.unwrap_or_default();
Some(apply_static_placeholders(
&template,
&StaticPlaceholderValues {
skills_summary: &skills_summary,
tools_summary: &tools_summary,
style_text: &style_text,
memory_text: &memory_text,
soul_text: &soul_text,
agent_md_text: &agent_md_text,
current_dir: ¤t_dir,
skill_dir: &skill_dir,
project_skill_dir: &project_skill_dir,
},
))
});
let hook_manager_clone = match self.hook_manager.lock() {
Ok(manager) => manager.clone(),
Err(_) => HookManager::default(),
};
let todo_manager = Arc::clone(&self.todo_manager);
{
let mut shared = safe_lock(&self.ui_messages, "spawn_agent::clear_shared");
shared.clear();
}
self.ui_messages_read_offset = 0;
let agent_config = AgentLoopConfig {
provider,
max_llm_rounds,
compact_config,
hook_manager: hook_manager_clone,
cancel_token: CancellationToken::new(),
};
let agent_shared = AgentLoopSharedState {
streaming_content,
pending_user_messages,
background_manager,
todo_manager,
ui_messages: Arc::clone(&self.ui_messages),
estimated_context_tokens: Arc::clone(&self.context_tokens),
invoked_skills: Arc::clone(&self.invoked_skills),
session_id: self.session_id.clone(),
};
let (handle, tool_result_tx) = MainAgentHandle::spawn(
agent_config,
agent_shared,
api_messages,
tools,
system_prompt_fn,
);
self.main_agent = Some(handle);
self.tool_executor.tool_result_tx = Some(tool_result_tx);
}
}