TUI: redirect stderr to log file and display in UI

Raw terminal mode swallows stderr output, making debugging difficult.
Now redirects stderr through a pipe to:
1. Log file at ~/.consciousness/logs/tui-stderr.log (persistent)
2. Channel polled by UI thread (shown as notifications)

The reader thread ensures both destinations see every line. Original
stderr is restored on exit so post-session errors reach the terminal.

Co-Authored-By: Proof of Concept <poc@bcachefs.org>
This commit is contained in:
Kent Overstreet 2026-04-15 01:58:34 -04:00
parent 4b710eb7a7
commit 6c28eebb3f

View file

@ -452,6 +452,18 @@ async fn run(
});
}
// Drain stderr lines and display as notifications
if let Some(rx_mutex) = STDERR_RX.get() {
if let Ok(rx) = rx_mutex.try_lock() {
while let Ok(line) = rx.try_recv() {
if let Ok(mut ag) = agent.state.try_lock() {
ag.notify(format!("stderr: {}", line));
dirty = true;
}
}
}
}
// Rebuild tools if requested (e.g., think tool toggled)
if app.rebuild_tools_pending {
app.rebuild_tools_pending = false;
@ -581,11 +593,95 @@ pub enum SubCmd {
},
}
/// Global stderr receiver — set once at startup, polled by UI thread.
static STDERR_RX: std::sync::OnceLock<std::sync::Mutex<std::sync::mpsc::Receiver<String>>> =
std::sync::OnceLock::new();
/// Redirect stderr to a pipe. Spawns a thread that writes to log file and sends
/// lines to a channel for display in the tools pane. Returns original stderr fd.
fn redirect_stderr_to_pipe() -> Option<std::os::fd::RawFd> {
use std::os::unix::io::FromRawFd;
use std::fs::OpenOptions;
use std::io::{BufRead, BufReader, Write};
let log_dir = dirs::home_dir()?.join(".consciousness/logs");
std::fs::create_dir_all(&log_dir).ok()?;
let log_path = log_dir.join("tui-stderr.log");
let mut log_file = OpenOptions::new()
.create(true)
.append(true)
.open(&log_path)
.ok()?;
// Create pipe
let mut pipe_fds = [0i32; 2];
if unsafe { libc::pipe(pipe_fds.as_mut_ptr()) } == -1 {
return None;
}
let (pipe_read, pipe_write) = (pipe_fds[0], pipe_fds[1]);
// Save original stderr
let original_stderr = unsafe { libc::dup(libc::STDERR_FILENO) };
if original_stderr == -1 {
unsafe { libc::close(pipe_read); libc::close(pipe_write); }
return None;
}
// Redirect stderr to pipe write end
if unsafe { libc::dup2(pipe_write, libc::STDERR_FILENO) } == -1 {
unsafe { libc::close(original_stderr); libc::close(pipe_read); libc::close(pipe_write); }
return None;
}
unsafe { libc::close(pipe_write); } // Close our copy, stderr now owns it
// Channel for UI display
let (tx, rx) = std::sync::mpsc::channel();
// Write startup marker
let timestamp = chrono::Local::now().format("%Y-%m-%d %H:%M:%S");
let marker = format!("\n--- TUI started at {} ---\n", timestamp);
let _ = log_file.write_all(marker.as_bytes());
// Spawn reader thread
std::thread::spawn(move || {
let pipe_read = unsafe { std::fs::File::from_raw_fd(pipe_read) };
let reader = BufReader::new(pipe_read);
for line in reader.lines() {
let line = match line {
Ok(l) => l,
Err(_) => break,
};
// Write to log file
let _ = writeln!(log_file, "{}", line);
let _ = log_file.flush();
// Send to UI (ignore if receiver dropped)
let _ = tx.send(line);
}
});
// Store receiver in static for UI thread access
let _ = STDERR_RX.set(std::sync::Mutex::new(rx));
Some(original_stderr)
}
/// Restore stderr to original fd (call on cleanup).
fn restore_stderr(original_fd: std::os::fd::RawFd) {
unsafe {
libc::dup2(original_fd, libc::STDERR_FILENO);
libc::close(original_fd);
}
}
#[tokio::main]
pub async fn main() {
// Auto-reap child processes (channel daemons outlive the supervisor)
unsafe { libc::signal(libc::SIGCHLD, libc::SIG_IGN); }
// Redirect stderr to pipe — logs to file and sends to channel for UI display
let stderr_capture = redirect_stderr_to_pipe();
// Initialize the Qwen tokenizer for direct token generation
let tokenizer_path = dirs::home_dir().unwrap_or_default()
.join(".consciousness/tokenizer-qwen35.json");
@ -606,7 +702,14 @@ pub async fn main() {
return;
}
if let Err(e) = start(cli).await {
let result = start(cli).await;
// Restore stderr before any terminal cleanup or error printing
if let Some(fd) = stderr_capture {
restore_stderr(fd);
}
if let Err(e) = result {
let _ = ratatui::crossterm::terminal::disable_raw_mode();
let _ = ratatui::crossterm::execute!(
std::io::stdout(),