fix: route warnings to file and add in-app toast notifications
- Replace all eprintln! with tracing::warn! to avoid TUI corruption via stderr writes during alternate screen mode - Route tracing output to ~/.local/share/zesdex/zesdex.log instead of stderr by configuring tracing_subscriber with a Mutex<File> writer - Add render_toasts() widget: floating notification stack at top-right of the terminal, color-coded by severity (Info/Success/Warning/Error/ Lesson), auto-expires after 5s, max 4 visible - Apply to 12 files: stream parser, MCP client, subagent engine, state/rest, controller/input, app_config, provider, OAuth, agent_def, dto/chat/tool Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.8
parent
e29dfadaa7
commit
89b63b1bc2
@@ -95,7 +95,7 @@ impl StdioChild {
|
||||
anyhow::bail!("MCP error: {}", err);
|
||||
}
|
||||
return Ok(resp.get("result").cloned().unwrap_or_else(|| {
|
||||
eprintln!("[mcp] stdio response missing 'result' field: {}", trimmed);
|
||||
tracing::warn!("[mcp] stdio response missing 'result' field: {}", trimmed);
|
||||
Value::Null
|
||||
}));
|
||||
}
|
||||
@@ -190,7 +190,7 @@ fn call_via_http(url: &str, tool_name: &str, tool_args: &Value) -> anyhow::Resul
|
||||
.connect_timeout(std::time::Duration::from_millis(MCP_CONNECT_TIMEOUT_MS))
|
||||
.build()
|
||||
.unwrap_or_else(|e| {
|
||||
eprintln!("[mcp] HTTP client builder failed: {}, using default client without timeouts", e);
|
||||
tracing::warn!("[mcp] HTTP client builder failed: {}, using default client without timeouts", e);
|
||||
reqwest::blocking::Client::new()
|
||||
});
|
||||
|
||||
@@ -214,7 +214,7 @@ fn call_via_http(url: &str, tool_name: &str, tool_args: &Value) -> anyhow::Resul
|
||||
if !resp.status().is_success() {
|
||||
let status = resp.status();
|
||||
let text = resp.text().unwrap_or_else(|e| {
|
||||
eprintln!("[mcp] failed to read HTTP response body: {}", e);
|
||||
tracing::warn!("[mcp] failed to read HTTP response body: {}", e);
|
||||
String::new()
|
||||
});
|
||||
anyhow::bail!("MCP HTTP server returned {}: {}", status, text);
|
||||
@@ -228,7 +228,7 @@ fn call_via_http(url: &str, tool_name: &str, tool_args: &Value) -> anyhow::Resul
|
||||
}
|
||||
|
||||
let result = response.get("result").cloned().unwrap_or_else(|| {
|
||||
eprintln!("[mcp] HTTP response missing 'result' field");
|
||||
tracing::warn!("[mcp] HTTP response missing 'result' field");
|
||||
Value::Null
|
||||
});
|
||||
extract_text_content(&result)
|
||||
@@ -250,7 +250,7 @@ fn extract_text_content(result: &Value) -> anyhow::Result<String> {
|
||||
}
|
||||
}
|
||||
Ok(serde_json::to_string_pretty(result).unwrap_or_else(|e| {
|
||||
eprintln!("[mcp] failed to pretty-print result: {}", e);
|
||||
tracing::warn!("[mcp] failed to pretty-print result: {}", e);
|
||||
result.to_string()
|
||||
}))
|
||||
}
|
||||
@@ -336,11 +336,11 @@ impl McpManager {
|
||||
Some(McpToolInfo {
|
||||
name: t.get("name")?.as_str()?.to_string(),
|
||||
description: t.get("description").and_then(|v| v.as_str()).unwrap_or_else(|| {
|
||||
eprintln!("[mcp] tool {} missing description", t.get("name").and_then(|n| n.as_str()).unwrap_or("?"));
|
||||
tracing::warn!("[mcp] tool {} missing description", t.get("name").and_then(|n| n.as_str()).unwrap_or("?"));
|
||||
""
|
||||
}).to_string(),
|
||||
input_schema: t.get("inputSchema").cloned().unwrap_or_else(|| {
|
||||
eprintln!("[mcp] tool {} missing inputSchema", t.get("name").and_then(|n| n.as_str()).unwrap_or("?"));
|
||||
tracing::warn!("[mcp] tool {} missing inputSchema", t.get("name").and_then(|n| n.as_str()).unwrap_or("?"));
|
||||
serde_json::Value::Null
|
||||
}),
|
||||
})
|
||||
|
||||
@@ -70,23 +70,23 @@ impl SseParser {
|
||||
let value: Value = match serde_json::from_str(&data) {
|
||||
Ok(v) => v,
|
||||
Err(e) => {
|
||||
eprintln!("[stream] failed to parse chunk: {}", e);
|
||||
tracing::warn!("[stream] failed to parse chunk: {}", e);
|
||||
return vec![];
|
||||
}
|
||||
};
|
||||
if let Some(usage) = value.get("usage") {
|
||||
if !usage.is_null() {
|
||||
let prompt_tokens = usage.get("prompt_tokens").and_then(|v| v.as_u64()).unwrap_or_else(|| {
|
||||
eprintln!("[stream] prompt_tokens missing in usage chunk");
|
||||
tracing::warn!("[stream] prompt_tokens missing in usage chunk");
|
||||
0
|
||||
});
|
||||
let completion_tokens = usage.get("completion_tokens").and_then(|v| v.as_u64()).unwrap_or_else(|| {
|
||||
eprintln!("[stream] completion_tokens missing in usage chunk");
|
||||
tracing::warn!("[stream] completion_tokens missing in usage chunk");
|
||||
0
|
||||
});
|
||||
let total_tokens = usage.get("total_tokens").and_then(|v| v.as_u64())
|
||||
.unwrap_or_else(|| {
|
||||
eprintln!("[stream] total_tokens missing in usage chunk");
|
||||
tracing::warn!("[stream] total_tokens missing in usage chunk");
|
||||
prompt_tokens + completion_tokens
|
||||
});
|
||||
return vec![StreamEvent::Usage { prompt_tokens, completion_tokens, total_tokens }];
|
||||
@@ -125,7 +125,7 @@ impl SseParser {
|
||||
let mut events = Vec::with_capacity(tool_calls.len());
|
||||
for tc in tool_calls {
|
||||
let index = tc.get("index").and_then(|i| i.as_u64()).unwrap_or_else(|| {
|
||||
eprintln!("[stream] tool call delta missing index, defaulting to 0");
|
||||
tracing::warn!("[stream] tool call delta missing index, defaulting to 0");
|
||||
0
|
||||
}) as usize;
|
||||
let id = tc.get("id").and_then(|i| i.as_str()).map(|s| s.to_string());
|
||||
@@ -202,7 +202,7 @@ pub fn parse_stream_chunk(data: &str) -> Option<StreamEvent> {
|
||||
if let Some(tool_calls) = delta.get("tool_calls").and_then(|tc| tc.as_array()) {
|
||||
if let Some(tc) = tool_calls.first() {
|
||||
let index = tc.get("index").and_then(|i| i.as_u64()).unwrap_or_else(|| {
|
||||
eprintln!("[stream] fallback parser: tool call missing index, defaulting to 0");
|
||||
tracing::warn!("[stream] fallback parser: tool call missing index, defaulting to 0");
|
||||
0
|
||||
}) as usize;
|
||||
let id = tc.get("id").and_then(|i| i.as_str()).map(|s| s.to_string());
|
||||
|
||||
@@ -62,11 +62,11 @@ impl AppStateRest {
|
||||
let settings = Settings::load();
|
||||
let app_config = AppConfig::load();
|
||||
let download_dir = memory_dir.parent().unwrap_or_else(|| {
|
||||
eprintln!("[state] memory_dir '{}' has no parent, using it for downloads", memory_dir.display());
|
||||
tracing::warn!("[state] memory_dir '{}' has no parent, using it for downloads", memory_dir.display());
|
||||
&memory_dir
|
||||
}).join("downloads");
|
||||
let worktrees_dir = memory_dir.parent().unwrap_or_else(|| {
|
||||
eprintln!("[state] memory_dir '{}' has no parent, using it for worktrees", memory_dir.display());
|
||||
tracing::warn!("[state] memory_dir '{}' has no parent, using it for worktrees", memory_dir.display());
|
||||
&memory_dir
|
||||
}).join("worktrees");
|
||||
let dir_cache = DirCache::new();
|
||||
@@ -74,7 +74,7 @@ impl AppStateRest {
|
||||
.file_name()
|
||||
.map(|n| n.to_string_lossy().to_string())
|
||||
.unwrap_or_else(|| {
|
||||
eprintln!("[state] session_dir has no file_name component, using empty session_id");
|
||||
tracing::warn!("[state] session_dir has no file_name component, using empty session_id");
|
||||
String::new()
|
||||
});
|
||||
AppStateRest {
|
||||
@@ -107,7 +107,7 @@ impl AppStateRest {
|
||||
|
||||
pub fn turn_in_flight(&self) -> bool {
|
||||
self.turn_in_flight.lock().map(|g| *g).unwrap_or_else(|_| {
|
||||
eprintln!("[state] turn_in_flight mutex poisoned");
|
||||
tracing::warn!("[state] turn_in_flight mutex poisoned");
|
||||
false
|
||||
})
|
||||
}
|
||||
@@ -133,11 +133,11 @@ impl AppStateRest {
|
||||
.and_then(|p| p.parent())
|
||||
.map(|p| p.to_path_buf())
|
||||
.unwrap_or_else(|| {
|
||||
eprintln!("[state] session_dir '{}' has no grandparent, using parent", self.session_dir.display());
|
||||
tracing::warn!("[state] session_dir '{}' has no grandparent, using parent", self.session_dir.display());
|
||||
self.session_dir.parent()
|
||||
.map(|p| p.to_path_buf())
|
||||
.unwrap_or_else(|| {
|
||||
eprintln!("[state] session_dir '{}' has no parent at all, using itself", self.session_dir.display());
|
||||
tracing::warn!("[state] session_dir '{}' has no parent at all, using itself", self.session_dir.display());
|
||||
self.session_dir.clone()
|
||||
})
|
||||
})
|
||||
|
||||
@@ -32,7 +32,7 @@ fn resolve_provider_config() -> (String, String, Option<String>) {
|
||||
let app_config = crate::model::app_config::AppConfig::load();
|
||||
|
||||
let mut api_key = settings.api_keys.get(&settings.provider).cloned().unwrap_or_else(|| {
|
||||
eprintln!("[subagent] no API key for provider '{}' in settings, trying env/default", settings.provider);
|
||||
tracing::warn!("[subagent] no API key for provider '{}' in settings, trying env/default", settings.provider);
|
||||
String::new()
|
||||
});
|
||||
let model = settings.model.clone();
|
||||
@@ -45,7 +45,7 @@ fn resolve_provider_config() -> (String, String, Option<String>) {
|
||||
.and_then(|env| std::env::var(env).ok())
|
||||
.or_else(|| provider_cfg.default_api_key.clone())
|
||||
.unwrap_or_else(|| {
|
||||
eprintln!("[subagent] all API key resolution paths exhausted for '{}'", settings.provider);
|
||||
tracing::warn!("[subagent] all API key resolution paths exhausted for '{}'", settings.provider);
|
||||
String::new()
|
||||
});
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user