RightNow-AI/openfang · error
Failed to draw
Error message
Failed to draw
What it means
The TUI main loop calls ratatui's `Terminal::draw`, which renders a frame and flushes it to the terminal backend. `draw` returns `io::Result` and the code unwraps it with `.expect("Failed to draw")`, so any I/O or terminal-control failure while painting a frame panics. Ratatui raises this when the terminal backend cannot perform the write or query the terminal state (e.g. stdout is not a real TTY).
Source
Thrown at crates/openfang-cli/src/tui/mod.rs:2423
let mut app = App::new(config, tx);
// Initial screen
if wizard::needs_setup() {
app.wizard.reset();
app.phase = Phase::Boot(BootScreen::Wizard);
} else {
app.phase = Phase::Boot(BootScreen::Welcome);
// Non-blocking daemon detection
app.start_daemon_detect();
}
// ── Main loop ────────────────────────────────────────────────────────────
// Draw first, then block on events. This ensures the first frame appears
// immediately, before any event processing.
while !app.should_quit {
terminal
.draw(|frame| app.draw(frame))
.expect("Failed to draw");
// Block until at least one event arrives (or 33ms timeout for ~30fps)
match rx.recv_timeout(Duration::from_millis(33)) {
Ok(ev) => app.handle_event(ev),
Err(mpsc::RecvTimeoutError::Timeout) => {}
Err(mpsc::RecvTimeoutError::Disconnected) => break,
}
// Drain all queued events immediately (batch processing)
while let Ok(ev) = rx.try_recv() {
app.handle_event(ev);
}
}
ratatui::restore();
}
View on GitHub (pinned to acf2587e46)
Solutions
- Run the binary in a real interactive terminal (no redirection/pipes on stdout).
- Add a TTY check before entering the TUI loop and fall back to a non-interactive mode if `atty::is(Stdout)` is false.
- Replace `.expect` with proper error propagation and exit gracefully with a message instead of panicking.
- If running over SSH, reconnect in a stable terminal and relaunch; ensure TERM is set correctly.
Example fix
// before
terminal
.draw(|frame| app.draw(frame))
.expect("Failed to draw");
// after
if !atty::is(atty::Stream::Stdout) {
eprintln!("TUI requires an interactive terminal");
return Err(anyhow!("stdout is not a TTY"));
}
terminal
.draw(|frame| app.draw(frame))
.context("TUI draw failed")?; Defensive patterns
Strategy: fallback
Validate before calling
fn can_draw_tui() -> bool {
atty::is(atty::Stream::Stdout)
&& std::env::var("TERM").map(|t| !t.is_empty()).unwrap_or(false)
}
// call before run_tui(); if false, use non-interactive mode Try / catch
// Rust has no try/catch; avoid expect and propagate
terminal.draw(|frame| app.draw(frame))
.context("TUI draw failed")?; Prevention
- Check stdout is a TTY before entering any ratatui loop
- Never pipe/redirect the TUI binary's stdout
- Handle resize and disconnect events gracefully
- Replace .expect on draw with error propagation and clean terminal restore
When it happens
Trigger: Running the CLI's TUI when stdout/stderr is redirected to a file or pipe (not a TTY), the terminal is closed/disconnected mid-loop, the descriptor hits an I/O error, or a raw-mode/alternate-screen restore fails so ratatui cannot write escape sequences.
Common situations: Developers piping `openfang` output to a log or `| tee`, running inside CI or non-interactive shells, running over SSH sessions that drop, or embedding the TUI in a test harness with a captured (non-tty) stdout.
Related errors
- Failed to draw
- draw failed
- MCP message too large: {content_length} bytes (max {MAX_MCP_
- Failed to set listener to non-blocking
AI-assisted analysis of RightNow-AI/openfang@acf2587e46 (2026-09-02).
Data as JSON: /api/errors/0f355af4505f20df.
Report an issue: GitHub.