use crate::command::chat::agent::config::{AgentLoopConfig, AgentLoopSharedState};
use crate::command::chat::app::AskRequest;
use crate::command::chat::app::MainAgentHandle;
use crate::command::chat::app::build_system_prompt_fn;
use crate::command::chat::app::types::{PlanDecision, StreamMsg, ToolResultMsg};
use crate::command::chat::context::compact::new_invoked_skills_map;
use crate::command::chat::context::window::select_messages;
use crate::command::chat::handler::run_chat_tui;
use crate::command::chat::infra::hook::{HookContext, HookEvent, HookManager};
use crate::command::chat::infra::skill;
use crate::command::chat::permission::{JcliConfig, generate_allow_rule};
use crate::command::chat::storage::{
AgentConfig, ChatMessage, MessageRole, ModelProvider, SessionEvent, ToolCallItem,
append_session_event, find_latest_session_id, load_agent_config, load_session,
};
use crate::command::chat::tools::ToolRegistry;
use crate::command::chat::tools::background::BackgroundManager;
use crate::command::chat::tools::classification::{ToolCategory, get_result_summary_for_tool};
use crate::command::chat::tools::task::TaskManager;
use crate::command::chat::tools::todo::TodoManager;
use crate::config::YamlConfig;
use crate::{error, info};
use std::io::{self, Write};
use std::sync::{Arc, Mutex, atomic::AtomicBool};
use std::time::Duration;
fn generate_oneshot_session_id() -> String {
use std::time::{SystemTime, UNIX_EPOCH};
let ts = SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap_or_default()
.as_micros();
let pid = std::process::id();
format!("{:x}-{:x}", ts, pid)
}
fn persist_messages(session_id: &str, messages: &[ChatMessage], start_idx: usize) {
for msg in messages.iter().skip(start_idx) {
append_session_event(session_id, &SessionEvent::msg(msg.clone()));
}
}
pub fn handle_chat(
content: &[String],
cont: bool,
session_id_opt: Option<&str>,
remote: bool,
port: u16,
bypass: bool,
_config: &YamlConfig,
) {
let agent_config = load_agent_config();
if remote
|| content.is_empty() && !cont && session_id_opt.is_none()
|| agent_config.providers.is_empty()
{
run_chat_tui(remote, port);
return;
}
let message = content.join(" ");
let message = message.trim().to_string();
if message.is_empty() && !cont && session_id_opt.is_none() {
error!("⚠️ 消息内容为空");
return;
}
if message.is_empty() {
error!("⚠️ 消息内容为空(--continue / --session 需要附带消息内容)");
return;
}
let session_id = if let Some(id) = session_id_opt {
id.to_string()
} else if cont {
find_latest_session_id().unwrap_or_else(generate_oneshot_session_id)
} else {
generate_oneshot_session_id()
};
let prior_messages = if cont || session_id_opt.is_some() {
let loaded = load_session(&session_id).messages;
if !loaded.is_empty() {
info!("📂 延续会话 {} ({} 条历史消息)", session_id, loaded.len());
} else if session_id_opt.is_some() {
info!("📂 会话 {} 不存在或为空,开始新对话", session_id);
}
loaded
} else {
vec![]
};
let idx = agent_config
.active_index
.min(agent_config.providers.len() - 1);
let provider = &agent_config.providers[idx];
info!("💫 [{}] 思考中...", provider.name);
if agent_config.tools_enabled {
run_oneshot_agent(
provider,
&agent_config,
message,
prior_messages,
&session_id,
bypass,
);
} else {
run_oneshot_no_tools(
provider,
&agent_config,
message,
prior_messages,
&session_id,
);
}
}
fn run_oneshot_no_tools(
provider: &ModelProvider,
agent_config: &AgentConfig,
message: String,
prior_messages: Vec<ChatMessage>,
session_id: &str,
) {
use crate::command::chat::agent::api::call_llm_stream;
use std::sync::atomic::{AtomicBool, Ordering};
let user_msg = ChatMessage::text(MessageRole::User, message.clone());
let mut messages = prior_messages.clone();
messages.push(user_msg.clone());
let term_width = crossterm::terminal::size()
.map(|(w, _)| w as usize)
.unwrap_or(80);
let mut cur_col: usize = 0;
let mut raw_lines: usize = 0;
let interrupted = Arc::new(AtomicBool::new(false));
let interrupted2 = Arc::clone(&interrupted);
let _ = ctrlc::set_handler(move || {
interrupted2.store(true, Ordering::Relaxed);
});
let send_messages = select_messages(
&messages,
agent_config.max_history_messages,
agent_config.max_context_tokens,
agent_config.compact.keep_recent,
&agent_config.compact.micro_compact_exempt_tools,
);
match call_llm_stream(
provider,
&send_messages,
crate::command::chat::storage::load_system_prompt().as_deref(),
&mut |chunk| {
if interrupted.load(Ordering::Relaxed) {
return;
}
print!("{}", chunk);
let _ = io::stdout().flush();
for ch in chunk.chars() {
if ch == '\n' {
raw_lines += 1;
cur_col = 0;
} else {
cur_col += 1;
if cur_col >= term_width {
raw_lines += 1;
cur_col = 0;
}
}
}
},
) {
Ok(full_text) => {
if !full_text.is_empty() {
redraw_markdown(raw_lines, cur_col, &full_text);
persist_messages(session_id, &[user_msg], 0);
persist_messages(
session_id,
&[ChatMessage::text(MessageRole::Assistant, &full_text)],
0,
);
use colored::Colorize;
eprintln!("{} {}", "会话 ID:".dimmed(), session_id.dimmed());
}
}
Err(e) => {
error!("\n✖️ {}", e.display_message());
}
}
}
fn redraw_markdown(raw_lines: usize, cur_col: usize, text: &str) {
use crossterm::{cursor, execute, terminal};
let total_raw_lines = if cur_col > 0 {
raw_lines + 1
} else {
raw_lines
};
let mut stdout = io::stdout();
if total_raw_lines > 0 {
let _ = execute!(stdout, cursor::MoveToColumn(0));
if total_raw_lines > 1 {
let _ = execute!(stdout, cursor::MoveUp((total_raw_lines - 1) as u16));
}
let _ = execute!(stdout, terminal::Clear(terminal::ClearType::FromCursorDown));
}
crate::util::md_render::render_md(text);
}
fn interactive_confirm(tool_msg: &str, options: &[&str], initial: usize) -> Option<usize> {
use colored::Colorize;
use crossterm::event::{self, Event, KeyCode};
use crossterm::{cursor, execute, terminal};
let mut stdout = io::stdout();
let mut cursor_pos = initial;
let total_lines = (1 + options.len() + 1) as u16;
let draw = |stdout: &mut io::Stdout,
cursor_pos: usize,
first: bool,
total_lines: u16|
-> io::Result<()> {
if !first {
let _ = execute!(stdout, cursor::MoveUp(total_lines));
}
let _ = execute!(stdout, terminal::Clear(terminal::ClearType::FromCursorDown));
write!(stdout, "{}\r\n", tool_msg)?;
for (i, opt) in options.iter().enumerate() {
let pointer = if cursor_pos == i { "❯" } else { " " };
let line = format!(" {} {}", pointer, opt);
if cursor_pos == i {
write!(stdout, "{}\r\n", line.cyan().bold())?;
} else {
write!(stdout, "{}\r\n", line.dimmed())?;
}
}
write!(
stdout,
" {} ↑↓ 移动 {} 确认\r\n",
"•".dimmed(),
"Enter".dimmed()
)?;
stdout.flush()?;
Ok(())
};
if terminal::enable_raw_mode().is_err() {
return None;
}
let _ = draw(&mut stdout, cursor_pos, true, total_lines);
let result = loop {
if let Ok(Event::Key(key)) = event::read() {
match key.code {
KeyCode::Up | KeyCode::Char('k') => {
cursor_pos = cursor_pos.saturating_sub(1);
}
KeyCode::Down | KeyCode::Char('j') => {
if cursor_pos + 1 < options.len() {
cursor_pos += 1;
}
}
KeyCode::Enter => break Some(cursor_pos),
KeyCode::Esc | KeyCode::Char('q') => break None,
_ => continue,
}
let _ = draw(&mut stdout, cursor_pos, false, total_lines);
}
};
let _ = terminal::disable_raw_mode();
{
let _ = execute!(stdout, cursor::MoveUp(total_lines));
let _ = execute!(stdout, terminal::Clear(terminal::ClearType::FromCursorDown));
}
result
}
fn redraw_streaming_as_markdown(
streaming_content: &Arc<Mutex<String>>,
raw_lines: &mut usize,
cur_col: &mut usize,
) {
let content = streaming_content.lock().unwrap();
if content.is_empty() {
return;
}
let term_width = crossterm::terminal::size()
.map(|(w, _)| w as usize)
.unwrap_or(80);
let mut rl: usize = 0;
let mut cc: usize = 0;
for ch in content.chars() {
if ch == '\n' {
rl += 1;
cc = 0;
} else {
cc += 1;
if cc >= term_width {
rl += 1;
cc = 0;
}
}
}
*raw_lines = rl;
*cur_col = cc;
redraw_markdown(*raw_lines, *cur_col, &content);
}
fn run_oneshot_agent(
provider: &ModelProvider,
agent_config: &AgentConfig,
message: String,
prior_messages: Vec<ChatMessage>,
session_id: &str,
bypass: bool,
) {
use colored::Colorize;
let hook_manager_loaded = HookManager::load();
let hook_manager_for_end = hook_manager_loaded.clone();
let disabled_hooks: Vec<String> = vec![];
{
if hook_manager_loaded.has_hooks_for(HookEvent::SessionStart) {
let ctx = HookContext {
event: HookEvent::SessionStart,
messages: Some(prior_messages.clone()),
model: Some(provider.model.clone()),
session_id: Some(session_id.to_string()),
..Default::default()
};
hook_manager_loaded.execute(HookEvent::SessionStart, ctx, &disabled_hooks);
}
}
let (ask_tx, ask_rx) = std::sync::mpsc::channel::<AskRequest>();
let background_manager = Arc::new(BackgroundManager::new());
let task_manager = Arc::new(TaskManager::new_with_session(session_id));
let todo_manager = Arc::new(TodoManager::new());
let hook_manager_for_registry = hook_manager_loaded.clone();
let invoked_skills = new_invoked_skills_map();
let tool_registry = Arc::new(ToolRegistry::new(
vec![],
ask_tx,
Arc::clone(&background_manager),
Arc::clone(&task_manager),
Arc::new(Mutex::new(hook_manager_for_registry)),
invoked_skills.clone(),
crate::command::chat::storage::SessionPaths::new(session_id).todos_file(),
));
std::thread::spawn(move || {
use colored::Colorize;
use crossterm::event::{self, Event, KeyCode};
use crossterm::{cursor, execute, terminal};
while let Ok(req) = ask_rx.recv() {
let mut answers = serde_json::Map::new();
for q in &req.questions {
if !q.header.is_empty() {
println!("\n❓ {}", q.header.cyan().bold());
}
if !q.question.is_empty() {
println!(" {}", q.question);
}
if q.multi_select {
let mut selected = vec![false; q.options.len()];
let mut cursor_pos: usize = 0;
let total_lines = (q.options.len() * 2 + 1) as u16;
let draw_multi = |stdout: &mut io::Stdout,
cursor_pos: usize,
selected: &[bool],
first: bool|
-> io::Result<()> {
if !first {
let _ = execute!(stdout, cursor::MoveUp(total_lines));
}
let _ =
execute!(stdout, terminal::Clear(terminal::ClearType::FromCursorDown));
for (i, opt) in q.options.iter().enumerate() {
let pointer = if cursor_pos == i { "❯" } else { " " };
let check = if selected[i] { "✓" } else { "○" };
let label_line = format!(" {} {} {}", pointer, check, opt.label);
let desc_line = format!(" {}", opt.description);
if cursor_pos == i {
write!(stdout, "{}\r\n", label_line.cyan().bold())?;
write!(stdout, "{}\r\n", desc_line.dimmed())?;
} else {
write!(stdout, "{}\r\n", label_line.dimmed())?;
write!(stdout, "{}\r\n", desc_line.dimmed())?;
}
}
write!(
stdout,
" {} ↑↓ 移动 {} 切换 {} 确认\r\n",
"•".dimmed(),
"Space".dimmed(),
"Enter".dimmed()
)?;
stdout.flush()?;
Ok(())
};
let _ = terminal::enable_raw_mode();
let mut stdout = io::stdout();
let _ = draw_multi(&mut stdout, cursor_pos, &selected, true);
loop {
if let Ok(Event::Key(key)) = event::read() {
match key.code {
KeyCode::Up | KeyCode::Char('k') => {
cursor_pos = cursor_pos.saturating_sub(1);
}
KeyCode::Down | KeyCode::Char('j') => {
if cursor_pos + 1 < q.options.len() {
cursor_pos += 1;
}
}
KeyCode::Char(' ') => {
selected[cursor_pos] = !selected[cursor_pos];
}
KeyCode::Enter => break,
KeyCode::Esc => break,
_ => continue,
}
let _ = draw_multi(&mut stdout, cursor_pos, &selected, false);
}
}
let _ = terminal::disable_raw_mode();
let _ = execute!(stdout, cursor::MoveUp(total_lines));
let _ = execute!(stdout, terminal::Clear(terminal::ClearType::FromCursorDown));
let result: Vec<String> = q
.options
.iter()
.zip(selected.iter())
.filter(|(_, s)| **s)
.map(|(o, _)| o.label.clone())
.collect();
let answer = if result.is_empty() {
"(无选择)".to_string()
} else {
result.join(", ")
};
println!(" → {}", answer.green());
answers.insert(q.header.clone(), serde_json::Value::String(answer));
} else {
let mut cursor_pos: usize = 0;
let total_lines = (q.options.len() * 2 + 1) as u16;
let draw_single = |stdout: &mut io::Stdout,
cursor_pos: usize,
first: bool|
-> io::Result<()> {
if !first {
let _ = execute!(stdout, cursor::MoveUp(total_lines));
}
let _ =
execute!(stdout, terminal::Clear(terminal::ClearType::FromCursorDown));
for (i, opt) in q.options.iter().enumerate() {
let pointer = if cursor_pos == i { "❯" } else { " " };
let label_line = format!(" {} {}", pointer, opt.label);
let desc_line = format!(" {}", opt.description);
if cursor_pos == i {
write!(stdout, "{}\r\n", label_line.cyan().bold())?;
write!(stdout, "{}\r\n", desc_line.dimmed())?;
} else {
write!(stdout, "{}\r\n", label_line.dimmed())?;
write!(stdout, "{}\r\n", desc_line.dimmed())?;
}
}
write!(
stdout,
" {} ↑↓ 移动 {} 确认\r\n",
"•".dimmed(),
"Enter".dimmed()
)?;
stdout.flush()?;
Ok(())
};
let _ = terminal::enable_raw_mode();
let mut stdout = io::stdout();
let _ = draw_single(&mut stdout, cursor_pos, true);
loop {
if let Ok(Event::Key(key)) = event::read() {
match key.code {
KeyCode::Up | KeyCode::Char('k') => {
cursor_pos = cursor_pos.saturating_sub(1);
}
KeyCode::Down | KeyCode::Char('j') => {
if cursor_pos + 1 < q.options.len() {
cursor_pos += 1;
}
}
KeyCode::Enter => break,
KeyCode::Esc => break,
_ => continue,
}
let _ = draw_single(&mut stdout, cursor_pos, false);
}
}
let _ = terminal::disable_raw_mode();
let _ = execute!(stdout, cursor::MoveUp(total_lines));
let _ = execute!(stdout, terminal::Clear(terminal::ClearType::FromCursorDown));
let answer = q
.options
.get(cursor_pos)
.map(|o| o.label.clone())
.unwrap_or_default();
println!(" → {}", answer.green());
answers.insert(q.header.clone(), serde_json::Value::String(answer));
}
}
let response = serde_json::to_string(&serde_json::json!({ "answers": answers }))
.unwrap_or_default();
let _ = req.response_tx.send(response);
}
});
let user_msg = ChatMessage::text(MessageRole::User, &message);
let prior_len = prior_messages.len();
let mut messages = prior_messages;
messages.push(user_msg);
let tools = tool_registry.to_llm_tools_filtered(&agent_config.disabled_tools);
let loaded_skills = skill::load_all_skills();
let system_prompt_fn = build_system_prompt_fn(
loaded_skills,
agent_config.disabled_skills.clone(),
agent_config.disabled_tools.clone(),
Arc::clone(&tool_registry),
);
let api_messages = select_messages(
&messages,
agent_config.max_history_messages,
agent_config.max_context_tokens,
agent_config.compact.keep_recent,
&agent_config.compact.micro_compact_exempt_tools,
);
let cancel_token = tokio_util::sync::CancellationToken::new();
let streaming_content: Arc<Mutex<String>> = Arc::new(Mutex::new(String::new()));
let streaming_reasoning_content: Arc<Mutex<String>> = Arc::new(Mutex::new(String::new()));
let pending_user_messages: Arc<Mutex<Vec<ChatMessage>>> = Arc::new(Mutex::new(vec![]));
let display_messages: Arc<Mutex<Vec<ChatMessage>>> = Arc::new(Mutex::new(vec![]));
let context_messages: Arc<Mutex<Vec<ChatMessage>>> = Arc::new(Mutex::new(vec![]));
let estimated_context_tokens: Arc<Mutex<usize>> = Arc::new(Mutex::new(0));
let derived_system_prompt: Arc<Mutex<Option<String>>> = Arc::new(Mutex::new(None));
let agent_config_struct = AgentLoopConfig {
provider: provider.clone(),
max_llm_rounds: agent_config.max_tool_rounds,
compact_config: agent_config.compact.clone(),
hook_manager: hook_manager_loaded,
disabled_hooks: agent_config.disabled_hooks.clone(),
cancel_token: cancel_token.clone(),
};
let agent_shared = AgentLoopSharedState {
streaming_content: Arc::clone(&streaming_content),
streaming_reasoning_content: Arc::clone(&streaming_reasoning_content),
pending_user_messages,
background_manager,
todo_manager,
display_messages: Arc::clone(&display_messages),
context_messages: Arc::clone(&context_messages),
estimated_context_tokens,
invoked_skills,
session_id: session_id.to_string(),
derived_system_prompt,
};
let cancel_for_ctrlc = cancel_token.clone();
let _ = ctrlc::set_handler(move || {
cancel_for_ctrlc.cancel();
});
let (handle, tool_result_tx) = MainAgentHandle::spawn(
agent_config_struct,
agent_shared,
api_messages,
tools,
system_prompt_fn,
);
let tool_result_tx: std::sync::mpsc::SyncSender<ToolResultMsg> = tool_result_tx;
let mut last_streaming_len: usize = 0;
let mut raw_lines: usize = 0;
let mut cur_col: usize = 0;
let term_width = crossterm::terminal::size()
.map(|(w, _)| w as usize)
.unwrap_or(80);
let jcli_config = JcliConfig::load();
let cancelled = Arc::new(AtomicBool::new(false));
let mut round: usize = 0;
let mut total_tools: usize = 0;
loop {
let msgs = handle.poll();
if msgs.is_empty() {
std::thread::sleep(Duration::from_millis(30));
continue;
}
for msg in msgs {
match msg {
StreamMsg::Chunk => {
let content = streaming_content.lock().unwrap();
if content.len() > last_streaming_len {
let delta = &content[last_streaming_len..];
print!("{}", delta);
let _ = io::stdout().flush();
for ch in delta.chars() {
if ch == '\n' {
raw_lines += 1;
cur_col = 0;
} else {
cur_col += 1;
if cur_col >= term_width {
raw_lines += 1;
cur_col = 0;
}
}
}
last_streaming_len = content.len();
}
}
StreamMsg::ToolCallRequest(items) => {
if last_streaming_len > 0 {
redraw_streaming_as_markdown(
&streaming_content,
&mut raw_lines,
&mut cur_col,
);
last_streaming_len = streaming_content.lock().unwrap().len();
}
round += 1;
total_tools += items.len();
eprintln!("\n{} R{} · {} 工具", "⚙".dimmed(), round, items.len(),);
for (i, item) in items.iter().enumerate() {
let tool_result = handle_tool_call(
item,
tool_registry.as_ref(),
&jcli_config,
&cancelled,
bypass,
i + 1,
items.len(),
);
let _ = tool_result_tx.send(tool_result);
}
}
StreamMsg::Done => {
if last_streaming_len > 0 {
redraw_streaming_as_markdown(
&streaming_content,
&mut raw_lines,
&mut cur_col,
);
}
let ctx_msgs = context_messages.lock().unwrap();
let persist_from = if prior_len < ctx_msgs.len() {
prior_len
} else {
0
};
persist_messages(session_id, &ctx_msgs, persist_from);
if round > 0 {
eprintln!("{} R{} · {} 工具", "✓".green(), round, total_tools);
}
eprintln!("{} {}", "会话 ID:".dimmed(), session_id.dimmed());
fire_session_end(
&hook_manager_for_end,
&disabled_hooks,
&ctx_msgs,
session_id,
&provider.model,
);
return;
}
StreamMsg::Error(e) => {
error!("\n{}", e.display_message());
let ctx_msgs = context_messages.lock().unwrap();
let persist_from = if prior_len < ctx_msgs.len() {
prior_len
} else {
0
};
persist_messages(session_id, &ctx_msgs, persist_from);
fire_session_end(
&hook_manager_for_end,
&disabled_hooks,
&ctx_msgs,
session_id,
&provider.model,
);
return;
}
StreamMsg::Cancelled => {
println!();
let ctx_msgs = context_messages.lock().unwrap();
let persist_from = if prior_len < ctx_msgs.len() {
prior_len
} else {
0
};
persist_messages(session_id, &ctx_msgs, persist_from);
eprintln!("\n{}", "⏹ 已中断".dimmed());
eprintln!("{} {}", "会话 ID:".dimmed(), session_id.dimmed());
fire_session_end(
&hook_manager_for_end,
&disabled_hooks,
&ctx_msgs,
session_id,
&provider.model,
);
return;
}
StreamMsg::Retrying {
attempt,
max_attempts,
delay_ms,
error,
} => {
eprintln!(
"{} 重试中 ({}/{}, {}ms) — {}",
"⟳".yellow(),
attempt,
max_attempts,
delay_ms,
error.dimmed()
);
}
StreamMsg::Compacting => {
eprintln!("{} 压缩上下文中...", "📦".dimmed());
}
StreamMsg::Compacted { messages_before } => {
eprintln!("{} 已压缩 {} 条消息", "📦".dimmed(), messages_before);
}
}
}
}
}
fn handle_tool_call(
item: &ToolCallItem,
tool_registry: &ToolRegistry,
jcli_config: &JcliConfig,
cancelled: &Arc<AtomicBool>,
bypass: bool,
idx: usize,
total: usize,
) -> ToolResultMsg {
use colored::Colorize;
let category = ToolCategory::from_name(&item.name);
let icon = category.icon();
let multi_prefix = if total > 1 {
format!("{}/{} ", idx, total)
} else {
String::new()
};
if jcli_config.is_denied(&item.name, &item.arguments) {
eprintln!(
" {} {}{}{}",
"✗".red(),
multi_prefix,
item.name.red().bold(),
" — 被权限规则拒绝".red()
);
return ToolResultMsg {
tool_call_id: item.id.clone(),
result: "工具调用被拒绝(deny 规则匹配)".to_string(),
is_error: true,
images: vec![],
plan_decision: PlanDecision::None,
};
}
let needs_confirm = tool_registry
.get(&item.name)
.map(|t| t.requires_confirmation())
.unwrap_or(false)
&& !jcli_config.is_allowed(&item.name, &item.arguments);
if needs_confirm && !bypass {
let tool_desc = format!(" {} {} {}", multi_prefix, icon, item.name.cyan());
let allow_rule = generate_allow_rule(&item.name, &item.arguments);
let options = ["允许执行", "拒绝", &format!("始终允许 ({})", allow_rule)];
let choice = interactive_confirm(&tool_desc, &options, 0);
match choice {
Some(0) => {}
Some(2) => {
}
_ => {
eprintln!(
" {} {}{}{}",
"⏭".dimmed(),
multi_prefix,
item.name.dimmed(),
" — 已跳过".dimmed()
);
return ToolResultMsg {
tool_call_id: item.id.clone(),
result: "用户拒绝执行该工具".to_string(),
is_error: true,
images: vec![],
plan_decision: PlanDecision::None,
};
}
}
} else {
eprintln!(" {} {} {}", multi_prefix, icon, item.name.cyan());
}
let start = std::time::Instant::now();
let result = tool_registry.execute(&item.name, &item.arguments, cancelled);
let elapsed = start.elapsed();
let elapsed_str = format_duration(elapsed);
let summary = get_result_summary_for_tool(
&result.output,
result.is_error,
&item.name,
Some(&item.arguments),
);
if result.is_error {
eprintln!(
" {} {}{} · {}",
"✗".red(),
multi_prefix,
"失败".red(),
elapsed_str.dimmed()
);
} else {
eprintln!(
" {} {}{} · {}",
"✓".green(),
multi_prefix,
summary.green(),
elapsed_str.dimmed()
);
}
ToolResultMsg {
tool_call_id: item.id.clone(),
result: result.output,
is_error: result.is_error,
images: vec![],
plan_decision: PlanDecision::None,
}
}
fn format_duration(d: std::time::Duration) -> String {
let ms = d.as_millis();
if ms < 1000 {
format!("{}ms", ms)
} else {
format!("{:.1}s", d.as_secs_f64())
}
}
fn fire_session_end(
hook_manager: &HookManager,
disabled_hooks: &[String],
messages: &[ChatMessage],
session_id: &str,
model: &str,
) {
if hook_manager.has_hooks_for(HookEvent::SessionEnd) {
let ctx = HookContext {
event: HookEvent::SessionEnd,
messages: Some(messages.to_vec()),
model: Some(model.to_string()),
session_id: Some(session_id.to_string()),
..Default::default()
};
hook_manager.execute(HookEvent::SessionEnd, ctx, disabled_hooks);
}
}