From 6c28eebb3f5e8c2896dfc062b52967223c0e9397 Mon Sep 17 00:00:00 2001 From: Kent Overstreet Date: Wed, 15 Apr 2026 01:58:34 -0400 Subject: [PATCH] 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 --- src/user/mod.rs | 105 +++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 104 insertions(+), 1 deletion(-) diff --git a/src/user/mod.rs b/src/user/mod.rs index b9a5037..09e485f 100644 --- a/src/user/mod.rs +++ b/src/user/mod.rs @@ -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::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 { + 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(),