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:
parent
4b710eb7a7
commit
6c28eebb3f
1 changed files with 104 additions and 1 deletions
105
src/user/mod.rs
105
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)
|
// Rebuild tools if requested (e.g., think tool toggled)
|
||||||
if app.rebuild_tools_pending {
|
if app.rebuild_tools_pending {
|
||||||
app.rebuild_tools_pending = false;
|
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]
|
#[tokio::main]
|
||||||
pub async fn main() {
|
pub async fn main() {
|
||||||
// Auto-reap child processes (channel daemons outlive the supervisor)
|
// Auto-reap child processes (channel daemons outlive the supervisor)
|
||||||
unsafe { libc::signal(libc::SIGCHLD, libc::SIG_IGN); }
|
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
|
// Initialize the Qwen tokenizer for direct token generation
|
||||||
let tokenizer_path = dirs::home_dir().unwrap_or_default()
|
let tokenizer_path = dirs::home_dir().unwrap_or_default()
|
||||||
.join(".consciousness/tokenizer-qwen35.json");
|
.join(".consciousness/tokenizer-qwen35.json");
|
||||||
|
|
@ -606,7 +702,14 @@ pub async fn main() {
|
||||||
return;
|
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::terminal::disable_raw_mode();
|
||||||
let _ = ratatui::crossterm::execute!(
|
let _ = ratatui::crossterm::execute!(
|
||||||
std::io::stdout(),
|
std::io::stdout(),
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue