diff --git a/tycode-cli/src/commands.rs b/tycode-cli/src/commands.rs index 1220f46..69e700b 100644 --- a/tycode-cli/src/commands.rs +++ b/tycode-cli/src/commands.rs @@ -14,6 +14,19 @@ pub enum LocalCommandResult { pub fn handle_local_command(state: &mut State, input: &str) -> LocalCommandResult { match input.trim() { + "/timing" => { + state.show_timing = !state.show_timing; + LocalCommandResult::Handled { + msg: format!( + "Timings: {}", + if state.show_timing { + "enabled" + } else { + "disabled" + } + ), + } + } "/verbose" => { state.show_reasoning = !state.show_reasoning; LocalCommandResult::Handled { diff --git a/tycode-cli/src/interactive_app.rs b/tycode-cli/src/interactive_app.rs index ff8c646..e917081 100644 --- a/tycode-cli/src/interactive_app.rs +++ b/tycode-cli/src/interactive_app.rs @@ -255,6 +255,22 @@ impl InteractiveApp { ChatEvent::ProfilesList { .. } => { // CLI handles profiles via slash commands, ignore this event } + ChatEvent::TimingUpdate { + waiting_for_human, + ai_processing, + tool_execution, + } => { + let total = waiting_for_human + ai_processing + tool_execution; + if self.state.show_timing { + self.formatter.print_system(&format!( + "Timing => Human: {:.1}s, AI: {:.1}s, Tools: {:.1}s, Total: {:.1}s", + waiting_for_human.as_secs_f64(), + ai_processing.as_secs_f64(), + tool_execution.as_secs_f64(), + total.as_secs_f64(), + )); + } + } } Ok(()) } diff --git a/tycode-cli/src/state.rs b/tycode-cli/src/state.rs index 46ff355..ffd032e 100644 --- a/tycode-cli/src/state.rs +++ b/tycode-cli/src/state.rs @@ -1,4 +1,5 @@ #[derive(Default)] pub struct State { pub show_reasoning: bool, + pub show_timing: bool, } diff --git a/tycode-core/src/chat/actor.rs b/tycode-core/src/chat/actor.rs index 60162a8..b0ac063 100644 --- a/tycode-core/src/chat/actor.rs +++ b/tycode-core/src/chat/actor.rs @@ -38,11 +38,25 @@ pub enum TimingState { ExecutingTools, } -#[derive(Debug, Clone)] -pub struct TimingStats { +#[derive(Clone, Debug, Default)] +pub struct TimingStat { pub waiting_for_human: Duration, pub ai_processing: Duration, pub tool_execution: Duration, +} + +impl std::ops::AddAssign for TimingStat { + fn add_assign(&mut self, rhs: Self) { + self.waiting_for_human += rhs.waiting_for_human; + self.ai_processing += rhs.ai_processing; + self.tool_execution += rhs.tool_execution; + } +} + +#[derive(Debug, Clone)] +pub struct TimingStats { + message: TimingStat, + session: TimingStat, current_state: TimingState, state_start: Option, } @@ -50,16 +64,15 @@ pub struct TimingStats { impl TimingStats { fn new() -> Self { Self { - waiting_for_human: Duration::ZERO, - ai_processing: Duration::ZERO, - tool_execution: Duration::ZERO, + message: TimingStat::default(), + session: TimingStat::default(), current_state: TimingState::Idle, state_start: Some(Instant::now()), } } - pub fn total_time(&self) -> Duration { - self.waiting_for_human + self.ai_processing + self.tool_execution + pub fn session(&self) -> TimingStat { + self.session.clone() } } @@ -447,18 +460,23 @@ impl ActorState { let elapsed = start.elapsed(); match self.timing_stats.current_state { TimingState::WaitingForHuman => { - self.timing_stats.waiting_for_human += elapsed; + self.timing_stats.message.waiting_for_human += elapsed; } TimingState::ProcessingAI => { - self.timing_stats.ai_processing += elapsed; + self.timing_stats.message.ai_processing += elapsed; } TimingState::ExecutingTools => { - self.timing_stats.tool_execution += elapsed; + self.timing_stats.message.tool_execution += elapsed; } TimingState::Idle => {} } } + if matches!(new_state, TimingState::WaitingForHuman) { + let message = std::mem::replace(&mut self.timing_stats.message, TimingStat::default()); + self.timing_stats.session += message; + } + self.timing_stats.current_state = new_state; self.timing_stats.state_start = Some(Instant::now()); } @@ -517,6 +535,11 @@ async fn run_actor( } } + state.event_sender.send(ChatEvent::TimingUpdate { + waiting_for_human: state.timing_stats.message.waiting_for_human, + ai_processing: state.timing_stats.message.ai_processing, + tool_execution: state.timing_stats.message.tool_execution, + }); state.event_sender.set_typing(false); state.transition_timing_state(TimingState::WaitingForHuman); } diff --git a/tycode-core/src/chat/commands.rs b/tycode-core/src/chat/commands.rs index dc761f8..1e6e455 100644 --- a/tycode-core/src/chat/commands.rs +++ b/tycode-core/src/chat/commands.rs @@ -1,7 +1,7 @@ use crate::agents::catalog::AgentCatalog; use crate::ai::model::{Model, ModelCost}; use crate::ai::{ModelSettings, ReasoningBudget, TokenUsage, ToolUseData}; -use crate::chat::actor::{create_provider, resume_session}; +use crate::chat::actor::{create_provider, resume_session, TimingStat}; use crate::chat::ai::select_model_for_agent; use crate::chat::tools::{current_agent, current_agent_mut}; use crate::chat::{ @@ -585,23 +585,28 @@ async fn handle_cost_command(state: &ActorState) -> Vec { message.push_str(&format!(" Average per 1K tokens: ${avg_cost_per_1k:.6}\n")); } - let timing = &state.timing_stats; + let TimingStat { + waiting_for_human, + ai_processing, + tool_execution, + } = state.timing_stats.session(); + let total_time = waiting_for_human + ai_processing + tool_execution; message.push_str("\nTime Spent:\n"); message.push_str(&format!( " Waiting for human: {:>6.1}s\n", - timing.waiting_for_human.as_secs_f64() + waiting_for_human.as_secs_f64() )); message.push_str(&format!( " AI processing: {:>6.1}s\n", - timing.ai_processing.as_secs_f64() + ai_processing.as_secs_f64() )); message.push_str(&format!( " Tool execution: {:>6.1}s\n", - timing.tool_execution.as_secs_f64() + tool_execution.as_secs_f64() )); message.push_str(&format!( " Total session: {:>6.1}s\n", - timing.total_time().as_secs_f64() + total_time.as_secs_f64() )); vec![create_message(message, MessageSender::System)] diff --git a/tycode-core/src/chat/events.rs b/tycode-core/src/chat/events.rs index 2d0a5d0..e1aa060 100644 --- a/tycode-core/src/chat/events.rs +++ b/tycode-core/src/chat/events.rs @@ -3,6 +3,7 @@ use crate::persistence::session::SessionMetadata; use crate::tools::tasks::TaskList; use chrono::Utc; use serde::{Deserialize, Serialize}; +use std::time::Duration; use tokio::sync::mpsc; /// `ChatEvent` are the messages sent from the actor - the output of the actor. @@ -43,6 +44,11 @@ pub enum ChatEvent { ProfilesList { profiles: Vec, }, + TimingUpdate { + waiting_for_human: Duration, + ai_processing: Duration, + tool_execution: Duration, + }, Error(String), }