diff --git a/src/agent/api/mod.rs b/src/agent/api/mod.rs index d2016c9..26bb293 100644 --- a/src/agent/api/mod.rs +++ b/src/agent/api/mod.rs @@ -305,7 +305,7 @@ impl SseReader { pub(crate) fn new(ui_tx: &UiSender) -> Self { Self { line_buf: String::new(), - chunk_timeout: Duration::from_secs(60), + chunk_timeout: Duration::from_secs(crate::config::get().api_stream_timeout_secs), stream_start: Instant::now(), chunks_received: 0, sse_lines_parsed: 0, diff --git a/src/agent/tui.rs b/src/agent/tui.rs index ceb0937..c93bb86 100644 --- a/src/agent/tui.rs +++ b/src/agent/tui.rs @@ -311,6 +311,10 @@ pub struct App { activity: String, /// When the current turn started (for elapsed timer). turn_started: Option, + /// When the current LLM call started (for per-call timer). + call_started: Option, + /// Stream timeout for the current call (for display). + call_timeout_secs: u64, /// Whether to emit a ● marker before the next assistant TextDelta. needs_assistant_marker: bool, /// Number of running child processes (updated by main loop). @@ -392,6 +396,8 @@ impl App { }, activity: String::new(), turn_started: None, + call_started: None, + call_timeout_secs: 60, needs_assistant_marker: false, running_processes: 0, reasoning_effort: "none".to_string(), @@ -485,6 +491,12 @@ impl App { } } UiMessage::Activity(text) => { + if text.is_empty() { + self.call_started = None; + } else if self.activity.is_empty() || self.call_started.is_none() { + self.call_started = Some(std::time::Instant::now()); + self.call_timeout_secs = crate::config::get().api_stream_timeout_secs; + } self.activity = text; } UiMessage::Reasoning(text) => { @@ -874,10 +886,12 @@ impl App { } // Draw status bar with live activity indicator - let elapsed = self.turn_started.map(|t| t.elapsed()); - let timer = match elapsed { - Some(d) if !self.activity.is_empty() => format!(" {:.0}s", d.as_secs_f64()), - _ => String::new(), + let timer = if !self.activity.is_empty() { + let total = self.turn_started.map(|t| t.elapsed().as_secs()).unwrap_or(0); + let call = self.call_started.map(|t| t.elapsed().as_secs()).unwrap_or(0); + format!(" {}s, {}/{}s", total, call, self.call_timeout_secs) + } else { + String::new() }; let tools_info = if self.status.turn_tools > 0 { format!(" ({}t)", self.status.turn_tools) diff --git a/src/config.rs b/src/config.rs index c9c1521..b6c0ea5 100644 --- a/src/config.rs +++ b/src/config.rs @@ -54,6 +54,7 @@ pub struct ContextGroup { fn default_true() -> bool { true } fn default_context_window() -> usize { 128_000 } +fn default_stream_timeout() -> u64 { 60 } fn default_identity_dir() -> PathBuf { PathBuf::from(std::env::var("HOME").expect("HOME not set")).join(".consciousness/identity") } @@ -91,6 +92,9 @@ pub struct Config { /// Used to resolve API settings, not stored on Config #[serde(default)] agent_model: Option, + /// Stream chunk timeout in seconds (no data = timeout). + #[serde(default = "default_stream_timeout")] + pub api_stream_timeout_secs: u64, pub api_reasoning: String, pub agent_types: Vec, /// Surface agent timeout in seconds. @@ -138,6 +142,7 @@ impl Default for Config { api_key: None, api_model: None, api_context_window: default_context_window(), + api_stream_timeout_secs: default_stream_timeout(), agent_model: None, api_reasoning: "high".to_string(), agent_types: vec![