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

  1. Run the binary in a real interactive terminal (no redirection/pipes on stdout).
  2. Add a TTY check before entering the TUI loop and fall back to a non-interactive mode if `atty::is(Stdout)` is false.
  3. Replace `.expect` with proper error propagation and exit gracefully with a message instead of panicking.
  4. 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

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


AI-assisted analysis of RightNow-AI/openfang@acf2587e46 (2026-09-02). Data as JSON: /api/errors/0f355af4505f20df. Report an issue: GitHub.