configurable stream timeout, show per-call timer in status bar

Stream chunk timeout is now api_stream_timeout_secs in config
(default 60s). Status bar shows total turn time and per-call
time with timeout: "thinking... 45s, 12/60s".

Co-Authored-By: Proof of Concept <poc@bcachefs.org>
This commit is contained in:
Kent Overstreet 2026-04-02 18:46:27 -04:00
parent 13d9cc962e
commit 156626ae53
3 changed files with 24 additions and 5 deletions

View file

@ -305,7 +305,7 @@ impl SseReader {
pub(crate) fn new(ui_tx: &UiSender) -> Self { pub(crate) fn new(ui_tx: &UiSender) -> Self {
Self { Self {
line_buf: String::new(), 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(), stream_start: Instant::now(),
chunks_received: 0, chunks_received: 0,
sse_lines_parsed: 0, sse_lines_parsed: 0,

View file

@ -311,6 +311,10 @@ pub struct App {
activity: String, activity: String,
/// When the current turn started (for elapsed timer). /// When the current turn started (for elapsed timer).
turn_started: Option<std::time::Instant>, turn_started: Option<std::time::Instant>,
/// When the current LLM call started (for per-call timer).
call_started: Option<std::time::Instant>,
/// Stream timeout for the current call (for display).
call_timeout_secs: u64,
/// Whether to emit a ● marker before the next assistant TextDelta. /// Whether to emit a ● marker before the next assistant TextDelta.
needs_assistant_marker: bool, needs_assistant_marker: bool,
/// Number of running child processes (updated by main loop). /// Number of running child processes (updated by main loop).
@ -392,6 +396,8 @@ impl App {
}, },
activity: String::new(), activity: String::new(),
turn_started: None, turn_started: None,
call_started: None,
call_timeout_secs: 60,
needs_assistant_marker: false, needs_assistant_marker: false,
running_processes: 0, running_processes: 0,
reasoning_effort: "none".to_string(), reasoning_effort: "none".to_string(),
@ -485,6 +491,12 @@ impl App {
} }
} }
UiMessage::Activity(text) => { 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; self.activity = text;
} }
UiMessage::Reasoning(text) => { UiMessage::Reasoning(text) => {
@ -874,10 +886,12 @@ impl App {
} }
// Draw status bar with live activity indicator // Draw status bar with live activity indicator
let elapsed = self.turn_started.map(|t| t.elapsed()); let timer = if !self.activity.is_empty() {
let timer = match elapsed { let total = self.turn_started.map(|t| t.elapsed().as_secs()).unwrap_or(0);
Some(d) if !self.activity.is_empty() => format!(" {:.0}s", d.as_secs_f64()), let call = self.call_started.map(|t| t.elapsed().as_secs()).unwrap_or(0);
_ => String::new(), format!(" {}s, {}/{}s", total, call, self.call_timeout_secs)
} else {
String::new()
}; };
let tools_info = if self.status.turn_tools > 0 { let tools_info = if self.status.turn_tools > 0 {
format!(" ({}t)", self.status.turn_tools) format!(" ({}t)", self.status.turn_tools)

View file

@ -54,6 +54,7 @@ pub struct ContextGroup {
fn default_true() -> bool { true } fn default_true() -> bool { true }
fn default_context_window() -> usize { 128_000 } fn default_context_window() -> usize { 128_000 }
fn default_stream_timeout() -> u64 { 60 }
fn default_identity_dir() -> PathBuf { fn default_identity_dir() -> PathBuf {
PathBuf::from(std::env::var("HOME").expect("HOME not set")).join(".consciousness/identity") 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 /// Used to resolve API settings, not stored on Config
#[serde(default)] #[serde(default)]
agent_model: Option<String>, agent_model: Option<String>,
/// Stream chunk timeout in seconds (no data = timeout).
#[serde(default = "default_stream_timeout")]
pub api_stream_timeout_secs: u64,
pub api_reasoning: String, pub api_reasoning: String,
pub agent_types: Vec<String>, pub agent_types: Vec<String>,
/// Surface agent timeout in seconds. /// Surface agent timeout in seconds.
@ -138,6 +142,7 @@ impl Default for Config {
api_key: None, api_key: None,
api_model: None, api_model: None,
api_context_window: default_context_window(), api_context_window: default_context_window(),
api_stream_timeout_secs: default_stream_timeout(),
agent_model: None, agent_model: None,
api_reasoning: "high".to_string(), api_reasoning: "high".to_string(),
agent_types: vec![ agent_types: vec![